feat: validate endpoint integration, Expr quantity sizing, apply_func input field fix

- Add /api/v1/strategies/validate client to SwymClient; wire into agent loop
  before submission so all DSL errors are surfaced in one round-trip
- Update dsl-schema.json to upstream: quantity is now oneOf[DecimalString, Expr],
  ExprApplyFunc uses "input" field (renamed from "expr")
- Update prompts: document expression-based quantity sizing (fixed-fraction and
  ATR-based examples), fix apply_func to use "input" not "expr" throughout
- Remove unused ValidationError import

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 09:12:12 +02:00
parent 5146b3f764
commit 7e1ff51ae0
4 changed files with 171 additions and 24 deletions

View File

@@ -74,21 +74,36 @@ bars_since_entry — complete bars elapsed since position was opened
balance — free balance of a named asset (e.g. "usdt", "usdc")
### Quantity
Action quantity MUST be a fixed decimal string that parses as a floating-point number.
NEVER use an expression object for quantity — only plain decimal strings are accepted.
NEVER use placeholder strings like `"ATR_SIZED"`, `"FULL_BALANCE"`, `"percent_of_balance"`,
`"dynamic"`, `"all"`, or any non-numeric string — these will be rejected immediately.
To exit a position, use the SAME decimal string as the entry rule — the backtester matches
open quantity automatically. There is no "close all" concept; just repeat the entry quantity.
Action quantity is either a fixed decimal string **or** an Expr that evaluates to a number
at candle close. If the Expr returns None the order is skipped; negative values are clamped
to zero.
Size your quantity to represent roughly 510% of the initial $10,000 USDC balance at
approximate current prices. Reference values (adjust if market prices differ significantly):
- BTC: `"0.01"` ≈ $800 (8% of $10k)
- ETH: `"3.0"` ≈ $600 (6% of $10k)
- SOL: `"50.0"` ≈ $700 (7% of $10k)
Since strategies run across all instruments with the same quantity string, choose a value
appropriate for the primary instrument you are designing for. Avoid `"0.001"` — it produces
negligible exposure and makes results statistically meaningless.
**Preferred: expression-based sizing** — instrument-agnostic, scales automatically:
Fixed fraction of quote balance (5% of USDC balance, converted to base units at current price):
```json
{{"kind":"bin_op","op":"div",
"left":{{"kind":"bin_op","op":"mul",
"left":{{"kind":"balance","asset":"usdc"}},
"right":{{"kind":"literal","value":"0.05"}}}},
"right":{{"kind":"field","field":"close"}}}}
```
ATR-based risk sizing (risk $200 per trade, sized by volatility):
```json
{{"kind":"bin_op","op":"div",
"left":{{"kind":"literal","value":"200"}},
"right":{{"kind":"func","name":"atr","period":14}}}}
```
For exit rules, use `position_quantity` to close the exact open position:
```json
{{"kind":"position_quantity"}}
```
**Fixed strings are also valid** when you want a specific size, e.g. `"0.01"`.
NEVER use placeholder strings like `"ATR_SIZED"`, `"FULL_BALANCE"`, `"all"`, `"dynamic"` —
these are rejected immediately. Use an Expr or a plain decimal string.
### Multi-timeframe
Any expression can reference a different timeframe via "timeframe" field.
@@ -166,7 +181,7 @@ Common mistakes to NEVER make:
with `ApplyFuncName` values: `highest`, `lowest`, `sma`, `ema`, `wma`, `std_dev`, `sum`,
`bollinger_upper`, `bollinger_lower`.
- `volume` is a candle FIELD, not a func name. Access it as `{{"kind":"field","field":"volume"}}`.
To compute EMA of volume: `{{"kind":"apply_func","name":"ema","period":20,"expr":{{"kind":"field","field":"volume"}}}}`.
To compute EMA of volume: `{{"kind":"apply_func","name":"ema","period":20,"input":{{"kind":"field","field":"volume"}}}}`.
- `bollinger_upper` and `bollinger_lower` are FUNC NAMES, not Expr kinds. To compare close to the upper band:
`{{"kind":"compare","left":{{"kind":"field","field":"close"}},"op":">","right":{{"kind":"func","name":"bollinger_upper","period":20}}}}`
NEVER write `{{"kind":"bollinger_upper",...}}` — `bollinger_upper` is not an Expr kind.
@@ -374,7 +389,7 @@ The MACD line is `EMA(12) - EMA(26)`; the signal line is `EMA(9)` of the MACD li
}},
"right": {{
"kind": "apply_func", "name": "ema", "period": 9,
"expr": {{
"input": {{
"kind": "bin_op", "op": "sub",
"left": {{"kind": "func", "name": "ema", "period": 12}},
"right": {{"kind": "func", "name": "ema", "period": 26}}
@@ -403,7 +418,7 @@ The MACD line is `EMA(12) - EMA(26)`; the signal line is `EMA(9)` of the MACD li
}},
"right": {{
"kind": "apply_func", "name": "ema", "period": 9,
"expr": {{
"input": {{
"kind": "bin_op", "op": "sub",
"left": {{"kind": "func", "name": "ema", "period": 12}},
"right": {{"kind": "func", "name": "ema", "period": 26}}
@@ -434,9 +449,10 @@ The MACD line is `EMA(12) - EMA(26)`; the signal line is `EMA(9)` of the MACD li
}}
```
Key pattern: `apply_func` wraps any `Expr` tree, enabling EMA-of-expression (signal line),
WMA-of-expression (Hull MA), or std_dev-of-returns. There is NO native `macd` func name —
always compose it as `bin_op(sub, func(ema,12), func(ema,26))` as shown above.
Key pattern: `apply_func` wraps any `Expr` tree using the `"input"` field (NOT `"expr"`).
This enables EMA-of-expression (signal line), WMA-of-expression (Hull MA), or std_dev-of-returns.
There is NO native `macd` func name — always compose it as `bin_op(sub, func(ema,12), func(ema,26))` as shown above.
CRITICAL: `apply_func` uses `"input"`, not `"expr"`. Writing `"expr":` will be rejected by the API.
## Anti-patterns to avoid