# Declarative position sizing methods for the strategy DSL

## Context

The "scout" tool (agentic LLM strategist) struggles with computing exact order quantities via
expression trees. LLMs are good at specifying *intent* ("risk 1% per trade", "buy $500 worth")
but bad at constructing the correct `bin_op(div, bin_op(mul, balance, literal), field(close))`
tree. Adding named sizing methods lets the LLM say `{ "method": "fixed_sum", "amount": "500" }`
and the executor resolves it to a base-asset quantity at execution time using live price/balance.

## Design

### New `SizingMethod` enum (tagged by `"method"`)

Add a third variant to `QuantitySpec`:

```rust
#[serde(untagged)]
pub enum QuantitySpec {
    Fixed(Decimal),              // "0.001"
    Sizing(Box<SizingMethod>),   // { "method": "fixed_sum", ... } — tried before Expr
    Expr(Box<Expr>),             // { "kind": "bin_op", ... }
}
```

`Sizing` must come before `Expr` in the enum so serde's untagged deserializer tries it first.
Both are JSON objects but they use different tag keys (`"method"` vs `"kind"`), so there is no
ambiguity — an object with `"method"` won't deserialize as `Expr`, and vice versa.

```rust
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "method", rename_all = "snake_case")]
pub enum SizingMethod {
    /// Buy `amount` worth of quote asset. qty = amount / price
    FixedSum { amount: Decimal },

    /// Buy `percent`% of free quote balance. qty = balance * percent/100 / price
    PercentOfBalance { percent: Decimal },

    /// Buy `percent`% of total portfolio equity. qty = equity * percent/100 / price
    PercentOfEquity { percent: Decimal },

    /// Buy exactly `units` of base asset (semantic alias for Fixed, but explicit in intent)
    FixedUnits { units: Decimal },
}
```

### JSON examples

```json
// Buy $500 worth at current price
{ "method": "fixed_sum", "amount": "500" }

// Risk 2% of free USDC balance
{ "method": "percent_of_balance", "percent": "2" }

// 5% of total portfolio value
{ "method": "percent_of_equity", "percent": "5" }

// Exactly 0.01 BTC (explicit alias)
{ "method": "fixed_units", "units": "0.01" }
```

### Resolution in `rule_strategy.rs`

Add a `resolve_sizing()` function called from the existing quantity resolution block:

```rust
let base_quantity = match &rule.then.quantity {
    QuantitySpec::Fixed(d) => Some(*d),
    QuantitySpec::Sizing(s) => resolve_sizing(s, current_price, &balances, &state),
    QuantitySpec::Expr(e) => eval_expr(e, &ctx).map(|v| v.max(Decimal::ZERO)),
};
```

`resolve_sizing` computes the base-asset quantity:

- **`FixedSum`**: `amount / current_price`
- **`PercentOfBalance`**: look up the quote asset's free balance from the balances map.
  The quote asset is the second half of the trading pair (e.g., USDC in BTCUSDC).
  `balance * (percent / 100) / current_price`
- **`PercentOfEquity`**: sum all asset balances valued at current prices.
  `total_equity * (percent / 100) / current_price`
- **`FixedUnits`**: `Some(units)` directly

All methods return `Option<Decimal>` — returns `None` (skip order) if price is zero or
balance lookup fails. Negative results clamped to zero.

### Quote asset detection

For `PercentOfBalance`, we need the quote asset name. The instrument is available from
`instrument_state` — the instrument key contains the quote asset. We'll extract it from
the `InstrumentKey` or pass it explicitly. The trading pair format in barter is
`(base, quote)`, so the quote is accessible from the instrument's `Asset` fields.

### Schema & docs updates

- Update `assets/strategy/schema.json` — add `SizingMethod` definition to `Action.quantity.oneOf`
- Update `docs/strategy-dsl.md` — add "Position sizing methods" section with examples
- Update doc comment on `QuantitySpec` with sizing method examples

## Files to modify

1. **`crates/swym-dal/src/models/strategy_config.rs`** — add `SizingMethod` enum, update `QuantitySpec`
2. **`services/paper-executor/src/strategy/rule_strategy.rs`** — add `resolve_sizing()`, update quantity match
3. **`assets/strategy/schema.json`** — add sizing method definitions
4. **`docs/strategy-dsl.md`** — document the new sizing methods

## Future work (not in this PR)

- **`kelly`** — Kelly criterion: needs incremental win_rate/avg_win/avg_loss from trade history
  within the current run. Requires plumbing `TradingSummary` stats into `EvalCtx`.
- **`optimal_f`** — needs max historical loss per trade, same plumbing requirement.
- **`cppi` / `tipp`** — need floor value tracking and cushion calculation across bars.
  These are stateful across the run, requiring new per-instrument state in the strategy.

These are deferred because they need trade-history state that isn't currently available
in the evaluation context, and the simpler methods cover the highest-value use cases.

## Verification

```bash
cargo check                    # compile-time SQL + type checking
cargo clippy                   # lint

# Backtest with fixed_sum sizing:
curl -X POST .../api/v1/paper-runs -d '{
  "config": {
    "strategy": {
      "type": "rule_based",
      "candle_interval": "5m",
      "rules": [{
        "when": { "kind": "ema_crossover", "fast": 9, "slow": 21 },
        "then": { "side": "buy", "quantity": { "method": "fixed_sum", "amount": "500" } }
      }, {
        "when": { "kind": "ema_crossover", "fast": 21, "slow": 9 },
        "then": { "side": "sell", "quantity": { "method": "fixed_sum", "amount": "500" } }
      }]
    },
    ...
  }
}'
# Verify the run completes without errors and trades show correct sizing
```
