> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sequency.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Strategies

# Strategy Canvas & Intelligence API

**Strategy Canvas & Intelligence** is the R\&D API for defining, running, and analyzing trading strategies. Use this API to programmatically create rule-based strategy definitions (entry groups, exit controls, risk parameters), run backtests against historical market data, and monitor deployment health across backtest → paper → shadow → live phases.

Built on **FalkorDB** (trading knowledge graph), **ClickHouse** (time-series), and **PostgreSQL** (relational state), this API serves strategy engineers, quant researchers, and live traders. All endpoints require authentication: Supabase JWT (`Authorization: Bearer <token>`) for users, or `X-Internal-Secret` header for service principals.

## Base Path

```
https://api.sequency.sh/api/graph/v1
```

Auth: Requires Supabase JWT (user) OR `X-Internal-Secret` (service). Strict auth enforced in prod: missing credentials → `401 Unauthorized`.

***

## Strategy CRUD & Canvas

### POST /strategies

Create a new strategy with rule-based entry/exit logic, risk controls, and optional visual canvas layout.

* **Parameters:** none
* **Request body:** `StrategyCreate`
* **Response 201:** `StrategyResponse`

Creates an initial version snapshot in `strategy_versions` table. If `archetype` is set and `entry_groups` is empty, injects preset rules + stop-loss configs for the archetype (momentum, trend\_following, mean\_reversion, volatility, or custom).

### GET /strategies

List authenticated user's strategies (paginated), optionally with latest backtest metrics.

* **Query parameters:**
  * `limit` (integer, optional, default=20, max=100): Results per page
  * `offset` (integer, optional, default=0): Pagination offset
  * `include` (string, optional): Pass `metrics` to join latest backtest results
* **Response 200:**

```json theme={null}
{
  "strategies": [
    {
      "id": "uuid",
      "user_id": "uuid",
      "name": "string",
      "archetype": "momentum|trend_following|mean_reversion|volatility|custom|null",
      "instrument": "stocks|options|both",
      "timeframe": "scalping|intraday|swing",
      "automation_level": "monitor|supervised|autonomous",
      "universe": { /* UniverseConfig */ },
      "rules": { /* StrategyRules */ },
      "risk_controls": { /* RiskControls */ },
      "canvas_layout": { /* CanvasLayoutPersisted */ } | null,
      "contract_selection": { /* RuleBasedContractConfig|SpecificContractConfig */ } | null,
      "deployment_phase": "backtest|paper|shadow|live",
      "deployment_ramp_pct": 0..100,
      "version": integer,
      "is_active": boolean,
      "created_at": "ISO 8601 datetime",
      "updated_at": "ISO 8601 datetime",
      "latest_backtest": {
        "oos_sharpe_pooled": number | null,
        "oos_pass_rate": number | null,
        "oos_n_windows": integer | null,
        "total_trades": integer | null,
        "winning_trades": integer | null,
        "losing_trades": integer | null,
        "win_rate": number | null,
        "profit_factor": number | null,
        "max_drawdown_pct": number | null,
        "calmar_ratio": number | null,
        "sortino_ratio": number | null,
        "sharpe_ratio": number | null,
        "expectancy_pct": number | null,
        "avg_holding_minutes": number | null,
        "total_pnl_dollars": number | null,
        "initial_capital": number | null,
        "date_from": "ISO 8601 date" | null,
        "date_to": "ISO 8601 date" | null,
        "slippage_model": "string" | null,
        "regime_breakdown": { /* regime → {sharpe, hit_rate, ...} */ },
        "created_at": "ISO 8601 datetime" | null
      } | null
    }
    /* ... more strategies ... */
  ],
  "total": integer,
  "limit": integer,
  "offset": integer
}
```

When `?include=metrics` is present, each strategy includes `latest_backtest` (null if no runs). When absent, `latest_backtest` is omitted.

### GET /strategies/{strategy_id}

Get a single strategy by ID, optionally with latest backtest results.

* **Parameters:**
  * `strategy_id` (path, required, UUID): Strategy ID
  * `include` (query, optional): Pass `backtest` to include latest backtest run (includes equity curve)
* **Response 200:** Strategy object (same shape as POST response, but with optional `latest_backtest` including `equity_curve` field if requested)

### GET /strategies/{strategy_id}/backtest-history

Get all backtest runs for a strategy, newest first.

* **Parameters:**
  * `strategy_id` (path, required, UUID)
  * `limit` (query, optional, default=20, max=50): Number of runs to return
* **Response 200:**

```json theme={null}
{
  "runs": [
    {
      "id": "uuid",
      "date_from": "ISO 8601 date" | null,
      "date_to": "ISO 8601 date" | null,
      "initial_capital": number | null,
      "slippage_model": "string" | null,
      "symbols_tested": "string" | null,
      "total_trades": integer | null,
      "win_rate": number | null,
      "profit_factor": number | null,
      "sharpe_ratio": number | null,
      "sortino_ratio": number | null,
      "calmar_ratio": number | null,
      "max_drawdown_pct": number | null,
      "expectancy_pct": number | null,
      "total_pnl_dollars": number | null,
      "oos_sharpe_pooled": number | null,
      "oos_pass_rate": number | null,
      "oos_n_windows": integer | null,
      "avg_holding_minutes": number | null,
      "created_at": "ISO 8601 datetime" | null
    }
    /* ... more runs ... */
  ],
  "total": integer
}
```

### PUT /strategies/{strategy_id}

Update a strategy. Increments version and records snapshots in `strategy_versions` if rules or risk\_controls change.

* **Parameters:**
  * `strategy_id` (path, required, UUID)
* **Request body:** `StrategyUpdate` (all fields optional)
* **Response 200:** `StrategyResponse`

Fields not provided are left unchanged. Canvas layout, contract selection, or any other single field can be updated independently.

### DELETE /strategies/{strategy_id}

Soft-delete a strategy (sets `deleted_at` timestamp).

* **Parameters:**
  * `strategy_id` (path, required, UUID)
* **Response 204:** No content

***

## Strategy Deployment & Promotion

### GET /strategies/{strategy_id}/gates

Get promotion gate status for the current deployment phase.

* **Parameters:**
  * `strategy_id` (path, required, UUID)
* **Response 200:**

```json theme={null}
{
  "strategy_id": "uuid string",
  "current_phase": "backtest|paper|shadow|live",
  "message": "string (contextual message: reason why gate can't promote, or 'ready')",
  "gates": [
    {
      "name": "string",
      "passed": boolean,
      "current_value": number | string | null,
      "threshold": number | string | null,
      "message": "string (reason for pass/fail)"
    }
    /* ... more gate checks ... */
  ],
  "can_promote": boolean
}
```

**Backtest→paper gates (enforced):**

* Must have run a backtest (result\_row required)
* Backtest must have sufficient trade density for walk-forward validation (`oos_sharpe_pooled` not null)
* Pooled out-of-sample Sharpe ≥ 1.0
* Minimum 5 total trades
* At least one regime (from `regime_breakdown`) with positive Sharpe
* For options strategies: stricter thresholds

**Paper→shadow:** Not yet available; requires live paper trading metrics.

**Shadow→live:** No gates; operator approval required.

### POST /strategies/{strategy_id}/promote

Promote a strategy to the next deployment phase if gates pass.

* **Parameters:**
  * `strategy_id` (path, required, UUID)
* **Request body:** none
* **Response 200:** `StrategyResponse` with updated `deployment_phase` and (for live promotion) `deployment_ramp_pct = 10`

Promotion order: backtest → paper → shadow → live. Backtest→paper is gated; paper→shadow and shadow→live require manual approval + operator intent.

***

## Strategy Health & Capacity

### GET /strategy/health

Get current health snapshot of all 6 signal types (latest daily).

* **Parameters:** none
* **Response 200:**

```json theme={null}
{
  "health": [
    {
      "signal_type": "string",
      "trade_date": "ISO 8601 date",
      "win_rate_5d": number | null,
      "win_rate_20d": number | null,
      "win_rate_60d": number | null,
      "profit_factor_20d": number | null,
      "sharpe_20d": number | null,
      "pct_excellent": number | null,
      "pct_good": number | null,
      "pct_marginal": number | null,
      "pct_poor": number | null,
      "excellent_hit_rate": number | null,
      "signal_noise_ratio": number | null,
      "winner_loser_ratio": number | null,
      "regime_concentration": number | null,
      "days_until_gate_failure": integer | null
    }
    /* ... one per signal type ... */
  ],
  "alerts": [
    {
      "signal_type": "string",
      "days_until_gate_failure": integer,
      "severity": "critical|warning"
    }
    /* ... only signals trending toward gate failure ... */
  ]
}
```

Alerts flag signal types where `days_until_gate_failure < 180` (warning) or `< 30` (critical).

### GET /strategy/health/{signal_type}/history

Get 90-day (or custom) health history for a single signal type.

* **Parameters:**
  * `signal_type` (path, required): Signal type name
  * `days` (query, optional, default=90, max=365): Lookback window in days
* **Response 200:**

```json theme={null}
{
  "signal_type": "string",
  "history": [
    {
      "trade_date": "ISO 8601 date",
      "win_rate_5d": number | null,
      "win_rate_20d": number | null,
      "win_rate_60d": number | null,
      "profit_factor_20d": number | null,
      "sharpe_20d": number | null,
      "pct_excellent": number | null,
      "pct_good": number | null,
      "signal_noise_ratio": number | null,
      "days_until_gate_failure": integer | null
    }
    /* ... one per day, chronologically ... */
  ],
  "days": integer
}
```

### GET /strategy/capacity

Get capacity estimate: current utilization, max capital before Sharpe degrades to 1.0, binding constraint.

* **Parameters:** none
* **Response 200:**

```json theme={null}
{
  "current_utilization_pct": number (0-100),
  "estimated_max_capital": number | null,
  "binding_constraint": "slippage|position_limits|liquidity",
  "sharpe_at_current": number,
  "avg_slippage_bps": number,
  "total_trades_90d": integer,
  "recommendation": "string (human-readable capacity guidance)"
}
```

Analyzes 90-day backtest trades to estimate where slippage scales linearly with capital and pushes Sharpe below 1.0.

***

## Strategy Pause/Resume (Trade Execution)

### GET /trade/strategies/pauses

List all paused strategies (whether paused by session or indefinitely).

* **Parameters:** none
* **Response 200:** `StrategyPauseListResponse`

### POST /trade/strategies/{strategy_id}/pause

Pause a strategy (session or indefinite).

* **Parameters:**
  * `strategy_id` (path, required, UUID)
* **Request body:** `PauseStrategyRequest | null`
  ```json theme={null}
  {
    "pause_type": "session|indefinite"
  }
  ```
* **Response 200:** `StrategyPauseResponse`

### DELETE /trade/strategies/{strategy_id}/pause

Resume a paused strategy.

* **Parameters:**
  * `strategy_id` (path, required, UUID)
* **Response 204:** No content (shape inferred from handler — likely returns `{status: "resumed"}` or similar)

***

## Strategy Matching & Recommendation

### GET /stocks/{symbol}/strategies

Get matching trading strategies for a symbol based on current market conditions.

* **Parameters:**
  * `symbol` (path, required): Stock ticker (e.g. "NVDA")
* **Response 200:** `StrategyMatchResponse`

Returns a hardcoded strategy dict (not graph-based) evaluated against real-time indicators (RSI, IV percentile, trend stack, volume, etc.). Includes match score (0-1), direction (bullish/bearish/neutral), and requirement status for each match.

***

## Internal Acceptance & Registry

The following endpoints are for **operator/QA only** (`/api/internal/...` require `X-Internal-Secret`):

| Endpoint                                                 | Purpose                                                                                             |
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `GET /internal/acceptance/signals/funnel`                | Signal funnel by date range and regime                                                              |
| `GET /internal/acceptance/signals/gate-status`           | Current signal-level acceptance gate status                                                         |
| `GET /internal/acceptance/signals/quality/{signal_type}` | Quality metrics for a single signal type (rolling windows, by tier)                                 |
| `GET /internal/acceptance/signals/report`                | Full signal acceptance report (gates, Spearman correlation, tier win rates, slippage, daily volume) |
| `GET /internal/signals/registry`                         | Complete signal registry (types, categories, presets, hostile regimes, phase gates)                 |
| `GET /internal/signals/strategy-map`                     | Strategy map with co-occurrence counts (date range filtered)                                        |
| `POST /internal/strategy/recommend/{thesis_id}`          | Generate strategy JSON from thesis catalysts/risks (LLM-generated, stub endpoint)                   |

***

## Key Schemas

### StrategyRules

The core rule-engine encoding for entry and exit logic.

```json theme={null}
{
  "entry_groups": [
    {
      "id": "group-entry-1",
      "label": "Momentum setup (optional canvas label)",
      "action": "enter",
      "conditions": [
        {
          "id": "cond-001",
          "source": "indicator",
          "indicator": "rsi_14",
          "field": null,
          "operator": "gt",
          "value": 70,
          "type": "overbought",
          "params": null,
          "ref": null,
          "ref_offset": null
        },
        {
          "source": "price_action",
          "field": "close",
          "operator": "crosses_above",
          "value": null,
          "ref": "indicator.ema_9",
          "ref_offset": null
        }
      ]
    }
  ],
  "exit_groups": [
    {
      "id": "exit-1",
      "label": "Tactical exit",
      "action": "exit",
      "conditions": [
        {
          "source": "indicator",
          "indicator": "rsi_14",
          "operator": "lt",
          "value": 30
        }
      ]
    }
  ]
}
```

### RuleCondition (Entry/Exit Condition)

Encodes a single decision rule: compare a source value (indicator, price, graph field, etc.) against an operator and a value.

* **id** (string | null): Stable canvas node ID (auto-assigned on first open, max 64 chars)
* **source** (`ConditionSource`): Where the value comes from
  * `indicator`: Named technical indicator (RSI, MACD, ATR, EMAs, BBands, etc.)
  * `mse_signal`: Real-time signal from Market Signal Engine
  * `price_action`: Price/VWAP/pivots
  * `volume`: Volume profile, bar volume, relative volume
  * `options`: Options-derived (IV percentile, put/call ratios, unusual activity)
  * `graph_field`: FalkorDB stock attribute (e.g. `graph_field.iv_percentile_30d`)
  * `stop_loss`: Meta-condition for stop-loss rules (used in exit\_groups; see StopLossConfig)
  * `time_exit`: Time-of-day exit (HH:MM ET)
* **indicator** (string | null): Indicator name when source=indicator (e.g. "rsi\_14", "macd\_12\_26", "atr\_14")
* **field** (string | null): Field name when source=price\_action or graph\_field (e.g. "close", "vwap", "iv\_percentile\_30d")
* **operator** (`Operator`): Comparison operator
  * `gt`, `gte`, `lt`, `lte`: Numeric comparison
  * `eq`, `neq`: Equality
  * `in`, `not_in`: List membership
  * `crosses_above`, `crosses_below`: Crossing detection (requires 2-bar history)
* **value** (float | int | string | list | null): RHS of comparison. For `in`/`not_in`, must be a list. For `crosses_above/below`, can be null (use `ref` instead).
* **ref** (string | null): Alternative RHS: resolves to another indicator/field value
  * Format: `indicator.<name>.<key>` (e.g. `indicator.rsi_14`) or `price.<field>` (e.g. `price.vwap`) or `graph_field.<field>`
* **ref\_offset** (`RefOffset` | null): Arithmetic offset applied to `ref` (PR-3 feature, #944)
  * `op`: `+` or `-`
  * `coef`: Scaling factor (e.g. 2.0 for 2× ATR)
  * `ref`: Offset value (e.g. `indicator.atr_14`)
  * Resolves to: `base_ref ± coef * offset_ref` (e.g. `price.vwap + 2.0 * indicator.atr_14`)
* **params** (object | null): Indicator-specific params (e.g. `{period: 14, threshold: 0.7}`)
* **type** (string | null): Semantic type hint for canvas rendering (e.g. "overbought", "breakout")
* **update\_frequency** (string | null): How often the condition is evaluated ("tick", "bar", "daily", etc.)
* **staleness\_warn\_sec** (integer | null): Alert if condition value is older than N seconds

### RiskControls

Position sizing, stop-loss configuration, and portfolio limits.

```json theme={null}
{
  "sizing_method": "fixed_risk",
  "sizing_params": {
    "risk_pct": 0.01
  },
  "stop_losses": [
    {
      "id": "stop-atr-1",
      "type": "atr",
      "params": {
        "multiplier": 2.0
      }
    },
    {
      "type": "target",
      "params": {
        "target_atr": 3.0
      }
    },
    {
      "type": "time",
      "params": {
        "time_et": "15:30"
      }
    },
    {
      "type": "conviction_drop",
      "params": {
        "threshold": 20
      }
    },
    {
      "type": "regime_flip",
      "params": {
        "exit_regimes": ["VOLATILE_SELLOFF"]
      }
    },
    {
      "type": "trailing",
      "params": {
        "atr_multiple": 1.5,
        "activation_r": 0.5
      }
    }
  ],
  "max_position_pct": 0.05,
  "max_total_positions": 6,
  "max_same_direction": 3,
  "max_sector_concentration_pct": 0.40,
  "max_loss_per_trade_dollars": 500.0,
  "max_overnight_exposure_pct": 0.15,
  "daily_pnl_limit_pct": -0.02,
  "consecutive_loss_limit": 3,
  "drawdown_limit_pct": -0.08
}
```

**StopLossConfig** types (discriminated union):

* `atr`: ATR-based trailing stop. Params: `multiplier` (number)
* `trailing`: Activates after return threshold; locks in gains. Params: `atr_multiple`, `activation_r`
* `target`: Exit when profit reaches N ATRs. Params: `target_atr` (number)
* `conviction_drop`: Exit if entry conviction signal drops below threshold. Params: `threshold` (number)
* `regime_flip`: Exit if market regime shifts to hostile (e.g. from BULLISH to VOLATILE\_SELLOFF). Params: `exit_regimes` (list of regime names)
* `time`: Exit at fixed ET time-of-day (EOD, etc.). Params: `time_et` (HH:MM string)

### UniverseConfig

Defines which symbols/sectors a strategy trades.

```json theme={null}
{
  "type": "full_universe",
  "symbols": ["NVDA", "AAPL", "MSFT"] | null,
  "sector": "Technology" | null,
  "min_market_cap": 100000000000,
  "min_adv": 10000000,
  "min_price": 5.0,
  "max_price": 5000.0
}
```

### ContractSelectionConfig

Rule-based or specific contract picker for options strategies.

**RuleBasedContractConfig** (parameterized rules):

```json theme={null}
{
  "mode": "rule_based",
  "direction": "bullish|bearish|auto",
  "target_delta": 0.30,
  "delta_tolerance": 0.10,
  "dte_min": 7,
  "dte_max": 45,
  "option_type": "call|put|auto",
  "min_open_interest": 10,
  "max_spread_pct": 0.10,
  "prefer_weekly": false
}
```

**SpecificContractConfig** (explicit list):

```json theme={null}
{
  "mode": "specific",
  "contracts": [
    {
      "contract_symbol": "NVDA260221C00200000",
      "label": "ATM call Feb 21"
    }
  ],
  "expiry_handling": "close_and_notify|roll_same_params|manual"
}
```

### CanvasLayoutPersisted

Visual canvas state (node positions, viewport, active group).

```json theme={null}
{
  "nodes": {
    "entry-1:0": {
      "x": 100.5,
      "y": 200.3,
      "column": "signal"
    },
    "entry-1:1": {
      "x": 350.0,
      "y": 200.0,
      "column": "filter"
    },
    "stop-atr-1": {
      "x": 100.0,
      "y": 500.0,
      "column": "exit"
    }
  },
  "viewport": {
    "x": 0.0,
    "y": 0.0,
    "zoom": 1.0
  },
  "active_group_id": "entry-1",
  "column_hints": {
    "entry-1": "signal",
    "exit-1": "exit"
  }
}
```

Node IDs: `<group_id>:<condition_index>` for conditions, `stoploss:<index>` for stops. Columns are: `signal`, `filter`, `hub`, `exit`.

### StrategyCreate

Request to POST /strategies.

```json theme={null}
{
  "name": "My Momentum Breakout (required)",
  "archetype": "momentum|trend_following|mean_reversion|volatility|custom",
  "instrument": "stocks|options|both",
  "timeframe": "scalping|intraday|swing",
  "automation_level": "monitor|supervised|autonomous",
  "universe": { /* UniverseConfig */ },
  "rules": { /* StrategyRules */ },
  "risk_controls": { /* RiskControls */ },
  "canvas_layout": { /* CanvasLayoutPersisted */ } | null,
  "contract_selection": { /* RuleBasedContractConfig|SpecificContractConfig */ } | null
}
```

### StrategyResponse

Returned by GET/PUT/POST (all CRUD + promote).

```json theme={null}
{
  "id": "uuid",
  "user_id": "uuid",
  "name": "string",
  "archetype": "string|null",
  "instrument": "stocks|options|both",
  "timeframe": "scalping|intraday|swing",
  "automation_level": "monitor|supervised|autonomous",
  "universe": { /* object */ },
  "rules": { /* object */ },
  "risk_controls": { /* object */ },
  "canvas_layout": { /* object */ } | null,
  "contract_selection": { /* object */ } | null,
  "deployment_phase": "backtest|paper|shadow|live",
  "deployment_ramp_pct": 0..100,
  "version": integer (increments on rule/risk_controls change),
  "is_active": boolean,
  "created_at": "ISO 8601 datetime",
  "updated_at": "ISO 8601 datetime"
}
```

### StrategyMatchResponse

Returned by GET /stocks/{symbol}/strategies. Hardcoded strategy dict evaluated against real-time indicators.

```json theme={null}
{
  "symbol": "NVDA",
  "market_conditions": ["LOW_VIX", "UPTREND", "HIGH_VOLUME"],
  "matches": [
    {
      "strategy": "MomentumBreakout",
      "category": "momentum",
      "direction": "bullish",
      "match_score": 0.85,
      "requirements_met": [
        {
          "indicator": "trend_stack_valid",
          "threshold": 1.0,
          "actual": 1.0,
          "satisfied": true
        }
      ],
      "parameters": {
        "ideal_iv_rank": 30,
        "min_dte": 21,
        "max_dte": 45,
        "delta_target": 0.40,
        "position_size_modifier": 1.0
      }
    }
  ]
}
```

### Enumerations

**Instrument:** `stocks`, `options`, `both`

**Timeframe:** `scalping`, `intraday`, `swing`

**Archetype:** `mean_reversion`, `trend_following`, `momentum`, `volatility`, `custom`

**AutomationLevel:** `monitor` (alerts only), `supervised` (requires approval), `autonomous` (self-executing)

**DeploymentPhase:** `backtest`, `paper`, `shadow`, `live`

**ConditionSource:** `indicator`, `mse_signal`, `price_action`, `volume`, `options`, `graph_field`, `stop_loss`, `time_exit`

**Operator:** `gt`, `gte`, `lt`, `lte`, `eq`, `neq`, `in`, `not_in`, `crosses_above`, `crosses_below`

### Example Full Strategy JSON

```json theme={null}
{
  "name": "EQ-7: Two-sided ATR Breakout",
  "archetype": "momentum",
  "instrument": "stocks",
  "timeframe": "intraday",
  "automation_level": "supervised",
  "universe": {
    "type": "full_universe",
    "min_market_cap": 100_000_000_000,
    "min_adv": 10_000_000
  },
  "rules": {
    "entry_groups": [
      {
        "id": "bullish-break",
        "label": "Long Entry",
        "action": "enter",
        "conditions": [
          {
            "id": "cond-1",
            "source": "price_action",
            "field": "close",
            "operator": "crosses_above",
            "value": null,
            "ref": "price.vwap",
            "ref_offset": {
              "op": "+",
              "coef": 2.0,
              "ref": "indicator.atr_14"
            }
          },
          {
            "source": "indicator",
            "indicator": "rsi_14",
            "operator": "gte",
            "value": 50
          },
          {
            "source": "volume",
            "field": "bar_volume",
            "operator": "gt",
            "ref": "indicator.sma_20_volume",
            "ref_offset": {
              "op": "*",
              "coef": 1.2,
              "ref": "indicator.sma_20_volume"
            }
          }
        ]
      },
      {
        "id": "bearish-break",
        "label": "Short Entry",
        "action": "enter",
        "conditions": [
          {
            "source": "price_action",
            "field": "close",
            "operator": "crosses_below",
            "value": null,
            "ref": "price.vwap",
            "ref_offset": {
              "op": "-",
              "coef": 2.0,
              "ref": "indicator.atr_14"
            }
          }
        ]
      }
    ],
    "exit_groups": [
      {
        "id": "profit-target",
        "label": "Profit exit",
        "action": "exit",
        "conditions": [
          {
            "source": "price_action",
            "field": "high|low (depending on direction)",
            "operator": "gte|lte",
            "value": null,
            "ref": "price.entry",
            "ref_offset": {
              "op": "+",
              "coef": 3.0,
              "ref": "indicator.atr_14"
            }
          }
        ]
      }
    ]
  },
  "risk_controls": {
    "sizing_method": "fixed_risk",
    "sizing_params": {
      "risk_pct": 0.015
    },
    "stop_losses": [
      {
        "type": "atr",
        "params": {
          "multiplier": 1.5
        }
      },
      {
        "type": "time",
        "params": {
          "time_et": "15:45"
        }
      },
      {
        "type": "regime_flip",
        "params": {
          "exit_regimes": ["VOLATILE_SELLOFF"]
        }
      }
    ],
    "max_position_pct": 0.04,
    "max_total_positions": 4,
    "max_same_direction": 2,
    "max_sector_concentration_pct": 0.30,
    "max_loss_per_trade_dollars": 400.0,
    "max_overnight_exposure_pct": 0.10,
    "daily_pnl_limit_pct": -0.03,
    "consecutive_loss_limit": 2,
    "drawdown_limit_pct": -0.10
  },
  "contract_selection": null,
  "canvas_layout": null
}
```

***

## Notes for Strategy Engineers

* **Preset rules:** When creating with `archetype != custom` and empty `entry_groups`, the API injects rules from `ARCHETYPE_PRESETS` (e.g., momentum archetype gets RSI 70/30 + volume filters). Caller's `risk_controls` (sizing, position limits) are preserved; only stop losses are merged if not already present.
* **Versioning:** Strategy versions are snapshots in the `strategy_versions` table, keyed on `(strategy_id, version)`. Incremented whenever `rules` or `risk_controls` change (via PUT). Query `strategy_versions` to audit rule evolution.
* **Canvas persistence:** Node positions, viewport zoom, and active group are stored in `canvas_layout` JSONB. IDs are stable across edits (auto-assigned on first open if missing).
* **Rule validation:** All rules are validated at the Pydantic layer (bounds on list lengths, string lengths, numeric ranges) and then by `RuleValidator` (semantic checks: at least one entry, at least one exit, no cross-references to missing indicators).
* **Phase gates:** Backtest→paper gates are automated (`oos_sharpe_pooled ≥ 1.0`, regime diversity, etc.). Paper→shadow and shadow→live require human approval + operator intent.
* **Backtests:** Strategies are tested on-the-fly (no separate tape rebuild needed) by the backtest engine, which sources `market_data_1min` and aggregates to the requested timeframe. Results are persisted in `strategy_backtest_results` with metrics (Sharpe, win rate, drawdown, regime breakdown, equity curve).
