407 lines
14 KiB
Markdown
407 lines
14 KiB
Markdown
## Margin Position Editor Plan
|
|
|
|
### Goal
|
|
|
|
Replace the current Reduce / Add Margin prompt flow with a real Edit Position dialog, and add a true Increase Size capability for isolated margin positions.
|
|
|
|
---
|
|
|
|
### Product Rules
|
|
|
|
- One margin position per broker_key + symbol.
|
|
- Increase Size means adding exposure in the same direction to the existing position.
|
|
- Add Collateral changes risk only; it does not change size.
|
|
- Remove Collateral withdraws excess collateral when margin ratio permits.
|
|
- Reduce Size partially closes the position and realizes proportional P/L.
|
|
- Close Position fully exits the position.
|
|
|
|
**Increase Size Inputs (two modes):**
|
|
|
|
| Mode | User Supplies | System Derives |
|
|
|------|---------------|----------------|
|
|
| Collateral-first | `additional_collateral`, `execution_leverage` | `added_size`, everything else |
|
|
| Size-first | `additional_size` | `required_collateral` (at current price), everything else |
|
|
|
|
**System-derived values for both modes:**
|
|
- `added_size` or `required_collateral` (whichever wasn't supplied)
|
|
- `new_total_size`
|
|
- `new_average_entry`
|
|
- `new_total_collateral`
|
|
- `new_effective_leverage`
|
|
- `new_liquidation_price`
|
|
- `new_margin_ratio`
|
|
|
|
---
|
|
|
|
### Phase 1: Backend Model
|
|
|
|
**Paper Broker (`paper_margin_broker.py`):**
|
|
|
|
Add `increase_position()`:
|
|
```python
|
|
def increase_position(
|
|
self,
|
|
symbol: str,
|
|
additional_collateral: Optional[float] = None,
|
|
execution_leverage: Optional[float] = None,
|
|
additional_size: Optional[float] = None,
|
|
current_price: Optional[float] = None
|
|
) -> MarginPosition:
|
|
```
|
|
|
|
Implementation requirements:
|
|
- Accrue interest on existing position before merging (locks in interest to this point).
|
|
- Calculate added size from inputs (collateral-first or size-first mode).
|
|
- Merge into existing position using weighted average entry.
|
|
- Recalculate: `borrowed`, `collateral`, `liquidation_price`, `margin_ratio`, `effective_leverage`.
|
|
- Persist increase as a history event, not a separate position.
|
|
|
|
Rejection rules:
|
|
- Position does not exist for symbol.
|
|
- Inputs would flip side (use close then open instead).
|
|
- Insufficient available balance for collateral.
|
|
- Resulting leverage exceeds `_max_leverage`.
|
|
- Invalid parameter combinations (must supply either collateral+leverage OR size).
|
|
|
|
**Live Broker (`live_margin_broker.py`):**
|
|
|
|
Add `increase_position()` with same signature. Additional requirements:
|
|
- Transfer additional collateral to isolated margin account first.
|
|
- Place autoBorrow order for the additional size.
|
|
- Wait for fill confirmation before reporting success.
|
|
- On partial fill: sync actual state, return `partial: True` with real position snapshot.
|
|
- On order failure after collateral transfer: attempt rollback transfer.
|
|
|
|
---
|
|
|
|
### Phase 1b: Paper Broker Unit Tests
|
|
|
|
Before building preview endpoints, validate paper broker math:
|
|
|
|
```python
|
|
# tests/test_paper_margin_broker_increase.py
|
|
|
|
def test_increase_long_collateral_first():
|
|
"""Increase long position using collateral + leverage inputs."""
|
|
|
|
def test_increase_long_size_first():
|
|
"""Increase long position using direct size input."""
|
|
|
|
def test_increase_short_collateral_first():
|
|
"""Increase short position using collateral + leverage inputs."""
|
|
|
|
def test_increase_weighted_average_entry():
|
|
"""Entry price is correctly weighted across increases."""
|
|
|
|
def test_increase_accrues_interest_before_merge():
|
|
"""Interest is locked in before position is merged."""
|
|
|
|
def test_increase_recalculates_liquidation_price():
|
|
"""Liquidation price reflects new effective leverage."""
|
|
|
|
def test_increase_rejects_opposite_side():
|
|
"""Cannot increase a long with short-side inputs."""
|
|
|
|
def test_increase_rejects_insufficient_balance():
|
|
"""Fails cleanly when balance cannot cover collateral."""
|
|
|
|
def test_increase_rejects_excessive_leverage():
|
|
"""Fails when resulting leverage exceeds max."""
|
|
|
|
def test_increase_rejects_missing_position():
|
|
"""Fails when no position exists for symbol."""
|
|
|
|
def test_multiple_increases_then_close():
|
|
"""Full lifecycle: open, increase twice, close. P/L correct."""
|
|
```
|
|
|
|
---
|
|
|
|
### Phase 2: Preview APIs
|
|
|
|
All preview endpoints use broker logic so UI and execution stay aligned.
|
|
|
|
**Endpoints:**
|
|
|
|
| Method | Path | Purpose |
|
|
|--------|------|---------|
|
|
| POST | `/api/margin/positions/preview-increase` | Preview increase operation |
|
|
| POST | `/api/margin/positions/preview-reduce` | Preview partial close |
|
|
| POST | `/api/margin/positions/preview-add-margin` | Preview collateral addition |
|
|
| POST | `/api/margin/positions/preview-remove-margin` | Preview collateral withdrawal |
|
|
|
|
**Preview Request (increase):**
|
|
```json
|
|
{
|
|
"broker_key": "paper_margin",
|
|
"symbol": "BTC/USDT",
|
|
"mode": "collateral_first",
|
|
"additional_collateral": 500,
|
|
"execution_leverage": 3
|
|
}
|
|
```
|
|
Or size-first mode:
|
|
```json
|
|
{
|
|
"broker_key": "paper_margin",
|
|
"symbol": "BTC/USDT",
|
|
"mode": "size_first",
|
|
"additional_size": 0.05
|
|
}
|
|
```
|
|
|
|
**Preview Response Schema:**
|
|
```json
|
|
{
|
|
"preview_type": "increase",
|
|
"valid": true,
|
|
"current": {
|
|
"symbol": "BTC/USDT",
|
|
"side": "long",
|
|
"size": 0.1,
|
|
"entry_price": 60000,
|
|
"collateral": 1000,
|
|
"effective_leverage": 6,
|
|
"liquidation_price": 52000,
|
|
"margin_ratio": 85,
|
|
"unrealized_pnl": 150
|
|
},
|
|
"projected": {
|
|
"total_size": 0.15,
|
|
"average_entry": 61333.33,
|
|
"total_collateral": 1500,
|
|
"effective_leverage": 6.13,
|
|
"liquidation_price": 53200,
|
|
"margin_ratio": 82,
|
|
"added_size": 0.05,
|
|
"required_collateral": 500
|
|
},
|
|
"warnings": [
|
|
"Effective leverage will exceed 5x"
|
|
],
|
|
"errors": []
|
|
}
|
|
```
|
|
|
|
**Error response (invalid preview):**
|
|
```json
|
|
{
|
|
"preview_type": "increase",
|
|
"valid": false,
|
|
"current": { ... },
|
|
"projected": null,
|
|
"warnings": [],
|
|
"errors": ["Insufficient balance: need 500, have 200"]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Phase 3: Execution APIs
|
|
|
|
**New endpoint:**
|
|
|
|
| Method | Path | Purpose |
|
|
|--------|------|---------|
|
|
| POST | `/api/margin/positions/increase` | Execute position increase |
|
|
| POST | `/api/margin/positions/remove-margin` | Withdraw excess collateral |
|
|
|
|
**Keep existing:**
|
|
- POST `/api/margin/positions/reduce`
|
|
- POST `/api/margin/positions/add-margin`
|
|
- POST `/api/margin/positions/close`
|
|
|
|
**Execution requirements:**
|
|
- For live margin, do not report `success: true` until order is filled or clear partial-fill state is returned.
|
|
- Return actual position snapshot after sync, not optimistic projection.
|
|
- Include `order_id` and `client_order_id` for audit trail.
|
|
|
|
---
|
|
|
|
### Phase 4: UI
|
|
|
|
**Replace buttons:**
|
|
- Remove individual "Reduce" and "Add Margin" buttons from position cards.
|
|
- Add single "Edit" button per position.
|
|
- Keep "Close" as a separate prominent action (not buried in dialog).
|
|
|
|
**Create `edit_position_dialog.html`:**
|
|
|
|
Dialog structure:
|
|
```
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ Edit Position: BTC/USDT LONG [X] │
|
|
├─────────────────────────────────────────────────────────┤
|
|
│ CURRENT POSITION │
|
|
│ ┌────────────────────────────────────────────────────┐ │
|
|
│ │ Size: 0.1 BTC Entry: $60,000 │ │
|
|
│ │ Collateral: $1,000 Leverage: 6x │ │
|
|
│ │ Liq Price: $52,000 Health: 85% │ │
|
|
│ │ Unrealized P/L: +$150 │ │
|
|
│ └────────────────────────────────────────────────────┘ │
|
|
├─────────────────────────────────────────────────────────┤
|
|
│ [Increase Size] [Reduce Size] [Add Collateral] [Remove]│
|
|
├─────────────────────────────────────────────────────────┤
|
|
│ INCREASE SIZE │
|
|
│ ┌────────────────────────────────────────────────────┐ │
|
|
│ │ Mode: (•) Collateral + Leverage ( ) Direct Size │ │
|
|
│ │ │ │
|
|
│ │ Additional Collateral: [____500____] USDT │ │
|
|
│ │ Execution Leverage: [____3x_____] │ │
|
|
│ │ │ │
|
|
│ │ ─── PROJECTED ─── │ │
|
|
│ │ New Size: 0.15 BTC (+0.05) │ │
|
|
│ │ Avg Entry: $61,333 │ │
|
|
│ │ New Collateral: $1,500 │ │
|
|
│ │ Effective Leverage: 6.13x │ │
|
|
│ │ New Liq Price: $53,200 │ │
|
|
│ │ New Health: 82% │ │
|
|
│ │ │ │
|
|
│ │ ⚠ Effective leverage will exceed 5x │ │
|
|
│ └────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ [Cancel] [Confirm Increase]│
|
|
└─────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
**UI behavior:**
|
|
- Call preview endpoints on input change with **300ms debounce**.
|
|
- Show loading spinner in projected section while preview is in-flight.
|
|
- Disable submit button while request is in-flight.
|
|
- Show inline validation errors from preview response.
|
|
- Show warnings (yellow) vs errors (red) distinctly.
|
|
- On success: close dialog, refresh all panels.
|
|
- On partial fill (live): show warning modal with actual state, refresh panels.
|
|
|
|
**Do NOT:**
|
|
- Use `prompt()`.
|
|
- Use frontend-only liquidation math.
|
|
- Allow submit when preview shows `valid: false`.
|
|
|
|
---
|
|
|
|
### Phase 5: Trade Panel Integration
|
|
|
|
**Position cards:**
|
|
- Replace "Reduce" / "Add Margin" buttons with single "Edit" button.
|
|
- Keep "Close" button visible (not in dialog).
|
|
- Show current margin_ratio as health bar.
|
|
|
|
**Refresh after edits:**
|
|
- Positions list
|
|
- History/activity feed
|
|
- Account stats (balance, equity)
|
|
- P/L summary
|
|
|
|
**Activity/history rows for:**
|
|
- Position increased (show size delta, new avg entry)
|
|
- Position reduced (show size delta, realized P/L)
|
|
- Collateral added (show amount, new health)
|
|
- Collateral removed (show amount, new health)
|
|
- Position closed (show final P/L)
|
|
|
|
---
|
|
|
|
### History Schema
|
|
|
|
Record for each edit operation:
|
|
|
|
```python
|
|
@dataclass
|
|
class MarginPositionEdit:
|
|
id: str # UUID
|
|
broker_key: str
|
|
symbol: str
|
|
action_type: str # 'increase', 'reduce', 'add_margin', 'remove_margin', 'close'
|
|
side: str # 'long' or 'short'
|
|
timestamp: datetime
|
|
|
|
# Deltas
|
|
size_delta: float # Positive for increase, negative for reduce
|
|
collateral_delta: float # Positive for add, negative for remove
|
|
|
|
# Before state
|
|
size_before: float
|
|
entry_price_before: float
|
|
collateral_before: float
|
|
effective_leverage_before: float
|
|
liquidation_price_before: float
|
|
margin_ratio_before: float
|
|
|
|
# After state
|
|
size_after: float
|
|
entry_price_after: float
|
|
collateral_after: float
|
|
effective_leverage_after: float
|
|
liquidation_price_after: float
|
|
margin_ratio_after: float
|
|
|
|
# For reduce/close
|
|
execution_price: Optional[float]
|
|
realized_pnl: Optional[float]
|
|
interest_paid: Optional[float]
|
|
|
|
# Audit
|
|
order_id: Optional[str] # For live trades
|
|
client_order_id: Optional[str]
|
|
```
|
|
|
|
---
|
|
|
|
### Testing
|
|
|
|
**Paper broker:**
|
|
- Increase long (collateral-first mode)
|
|
- Increase long (size-first mode)
|
|
- Increase short
|
|
- Weighted average entry calculation
|
|
- Interest accrual before merge
|
|
- Add collateral
|
|
- Remove collateral (with min health check)
|
|
- Partial reduce
|
|
- Full close after multiple increases
|
|
- Preview matches execution values
|
|
|
|
**Live broker unit tests:**
|
|
- Full fill flow
|
|
- Partial fill handling
|
|
- Failed collateral transfer
|
|
- Failed order after collateral transfer → rollback
|
|
- Order timeout handling
|
|
|
|
**UI tests:**
|
|
- Dialog opens from position card Edit button
|
|
- Tab switching works
|
|
- Previews update on input (with debounce)
|
|
- Loading states shown correctly
|
|
- Submit disabled during in-flight
|
|
- Success closes dialog and refreshes panels
|
|
- Errors display inline
|
|
- Warnings display distinctly from errors
|
|
|
|
---
|
|
|
|
### Rollout Order
|
|
|
|
| Step | Scope | Deliverable |
|
|
|------|-------|-------------|
|
|
| 1 | Backend | Paper broker `increase_position()` |
|
|
| 2 | Backend | Paper broker unit tests (Phase 1b) |
|
|
| 3 | Backend | Preview endpoints (all four) |
|
|
| 4 | Backend | `remove_margin` execution endpoint |
|
|
| 5 | Frontend | Edit dialog (wired to paper margin) |
|
|
| 6 | Backend | Live broker `increase_position()` |
|
|
| 7 | Backend | Live broker `remove_margin()` |
|
|
| 8 | Testing | Live execution hardening, tiny-position edge cases |
|
|
| 9 | Frontend | Wire dialog to live margin |
|
|
| 10 | QA | End-to-end paper + live testing |
|
|
|
|
---
|
|
|
|
### Out of Scope (Future)
|
|
|
|
- Cross-margin mode support
|
|
- Margin mode switching (isolated ↔ cross)
|
|
- Batch position edits
|
|
- Conditional increase (limit orders for averaging in)
|