---
title: "Simulate Validate"
method: POST
path: "/simulate/validate"
---

# Simulate Validate

`POST /simulate/validate`

## Request

- Base URL: `https://api.pruviq.com` (the document declares no server; this is the origin it was published from)
- URL: `https://api.pruviq.com/simulate/validate`
- Auth: none declared

Offload OOS/Monte-Carlo validation to a worker thread (event-loop safe).

## Request body

- ValidateRequest: OOS validation + Monte Carlo request.
  - `record_provenance` boolean: Capture actual engine/input hashes for a fresh registry-strategy execution; bypass result cache, retain compute limits
  - `registry` CompletedRegistryValidationInput: Effective inputs recorded by a completed registry execution, not a replay receipt.
    - `request` SimulationRequest, required: 시뮬레이션 요청 `extra="forbid"` — 모르는 필드는 **거부**한다(2026-09-20, Q-B97). 왜: 이 파일에 `extra` 선언이 한 줄도 없어서 Pydantic v2 기본값 `ignore` 가 적용돼 있었다. 그래서 `{"params": {...}}` 처럼 한 겹 감싸 보내면 **전 필드가 기본값으로 떨어지고 HTTP 200 에 전혀 다른 답**이 나갔다(실측 27배 차이). 호출자는 자기 입력이 버려진 걸 알 길이 없다 — 오답이 성공처럼 보인다. 사람에게도 나쁘지만 **에이전트에게 특히 나쁘다**: 사람은 화면 숫자가 이상하면 멈추지만, 프로그램은 200 을 받으면 그대로 인용한다. 회귀 위험은 켜기 전에 실측했다 — `/simulate` 를 부르는 프론트 4곳의 전송 키 합집합 13개가 **전부 이 모델의 필드**다(모델에만 있는 8개는 무관): SimulatorPage(자동실행) 13 · StrategyDemo 5 · TrustGapPanel 9 · simRequest 10 빌더의 커스텀 전략 본문(`...customSnapshot`)은 이 엔드포인트가 아니라 `/backtest` 로 간다 — 그쪽 모델은 이 변경의 범위가 아니다.
      - `record_provenance` boolean: Capture actual engine/input hashes for a fresh registry-strategy execution; bypass result cache, retain compute limits
      - `strategy` string: Strategy ID
      - `direction` string, nullable: long | short | both (null = use strategy default)
      - `sl_pct` number: Stop Loss %
      - `tp_pct` number: Take Profit %
      - `max_bars` integer: Max holding period (bars)
      - `market_type` 'futures' | 'spot': spot | futures
      - `symbols` string[], nullable: Specific symbols (null = use top_n)
      - `symbol` string, nullable: Single-symbol alias for `symbols`. Auto-mapped (prevents silent ignore of a singular field).
      - `top_n` integer, nullable: Number of coins (null = top 50)
      - `include_stablecoins` boolean: Include stablecoins (USDC, DAI…) in the top_n universe. Default false — ~0 volatility → empty backtests. Ignored when `symbols` is set (explicit picks always honored).
      - `start_date` string, nullable: Backtest start date (YYYY-MM-DD)
      - `end_date` string, nullable: Backtest end date (YYYY-MM-DD)
      - `cutoff_utc` string, date-time, nullable: Only candles closed by this UTC instant; null = current closed hour. Does not freeze historical source revisions.
      - `timeframe` '1H' | '2H' | '4H' | '6H' | '12H' | '1D' | '1W': Candle timeframe: 1H, 2H, 4H, 6H, 12H, 1D, 1W
      - `compounding` boolean: True = reinvest profits (compound), False = fixed position size (simple)
      - `avoid_hours` integer[], nullable: UTC hours to avoid entering trades (0-23). null = use strategy default
      - `avoid_months` integer[], nullable: Months to avoid entering trades (1-12). null = no month filter
      - `min_vol_regime` number, nullable: Minimum ATR ratio (current ATR / 14-period ATR MA) to allow entry. e.g., 0.7 = skip low-volatility periods. null = no filter
      - `btc_trend_filter` boolean: If true, only allow LONG entries when BTC is above its 50-period SMA. Improves LONG edge in bull regimes.
      - `fee_pct` number, nullable: Fee per side (0.0005 = 0.05%). None = use market_type default
      - `leverage` number: Leverage multiplier for PnL calculation
    - `resolved_strategy` string, required
    - `slippage_pct_per_side` object, required
  - `strategy` string: Strategy ID (ignored when `entry` is provided — custom-conditions mode)
  - `direction` string, nullable
  - `sl_pct` number
  - `tp_pct` number
  - `max_bars` integer
  - `market_type` 'futures' | 'spot'
  - `top_n` integer, nullable: Number of coins (null = top 50)
  - `symbols` string[], nullable
  - `oos_pct` number: OOS split %
  - `mc_runs` integer: Monte Carlo runs
  - `start_date` string, nullable: Backtest start date (YYYY-MM-DD)
  - `end_date` string, nullable: Backtest end date (YYYY-MM-DD)
  - `timeframe` '1H' | '2H' | '4H' | '6H' | '12H' | '1D' | '1W': Candle timeframe: 1H, 2H, 4H, 6H, 12H, 1D, 1W
  - `avoid_hours` integer[], nullable: UTC hours to avoid entering trades (0-23). null = use strategy default
  - `avoid_months` integer[], nullable: Months to avoid entering trades (1-12). null = no month filter
  - `min_vol_regime` number, nullable: Minimum ATR ratio (current ATR / 14-period ATR MA) to allow entry. e.g., 0.7 = skip low-volatility periods. null = no filter
  - `name` string, nullable: Custom strategy display name (custom-conditions mode only)
  - `indicators` object, nullable: Indicator configs for custom-conditions mode: {'bb': {'period': 20}, 'ema': {}, ...}
  - `entry` object, nullable: Entry conditions tree: {'type': 'AND', 'conditions': [...]}. Presence switches validation to custom-conditions mode (strategy field ignored).
  - `exit_conditions` object, nullable: Optional exit-signal conditions tree (custom-conditions mode only), same shape as entry.
  - `tp_source` 'custom_pct' | 'trailing': Exit mode (custom-conditions mode only): custom_pct = fixed TP/SL; trailing = trailing stop (tp_pct is the trail distance)
  - `trailing_pct` number, nullable: Trailing stop distance % (custom-conditions mode only). If null and tp_source=trailing, uses tp_pct.

## Response `200`

Successful Response

- ValidateResponse: OOS validation + Monte Carlo response.
  - `provenance` ExecutionProvenance
    - `schema_version` '1.0.0'
    - `engine_commit` string, required
    - `started_at` string, date-time, required
    - `measured_at` string, date-time, required
    - `request` object, required: Validated request with defaults before engine normalization
    - `coins` string[], required
    - `data_snapshot_id` string, required
    - `selection` SelectedFrame[], required
      - `snapshot_id` string, required
      - `rows` integer, required
      - `columns` FrameColumn[], required
        - `name` string, required
        - `dtype` string, required
      - `row_hash` string, required
      - `pandas_version` string, required
      - `period` FramePeriod, required
        - `start` string, nullable, required
        - `end` string, nullable, required
      - `symbol` string, required
    - `runs` EngineRun[], required
      - `parameters` object, required: Bound run_fast arguments, including defaults. Fee/slippage are per-side fractions; SL/TP are fractions.
      - `strategy_parameters` object, required
      - `strategy_config` object: All public instance attributes of the strategy as constructed (superset of strategy_parameters)
      - `data` FrameSnapshot, required
        - `snapshot_id` string, required
        - `rows` integer, required
        - `columns` FrameColumn[], required
          - `name` string, required
          - `dtype` string, required
        - `row_hash` string, required
        - `pandas_version` string, required
        - `period` FramePeriod, required
          - `start` string, nullable, required
          - `end` string, nullable, required
    - `funding` FundingSnapshot[], required
      - `symbol` string, required
      - `available` boolean, required
      - `rows` integer, nullable
      - `snapshot_id` string, nullable
    - `margin` MarginSnapshot[]: Liquidation maintenance-margin inputs used (leverage>1 runs)
      - `symbol` string, required
      - `mmr` number, required
      - `source` string, required
      - `tiers_sha256` string, nullable
      - `fallback_mmr` number, required
      - `taker_fee_buffer` number, required
  - `registry` CompletedRegistryValidationInput: Effective inputs recorded by a completed registry execution, not a replay receipt.
    - `request` SimulationRequest, required: 시뮬레이션 요청 `extra="forbid"` — 모르는 필드는 **거부**한다(2026-09-20, Q-B97). 왜: 이 파일에 `extra` 선언이 한 줄도 없어서 Pydantic v2 기본값 `ignore` 가 적용돼 있었다. 그래서 `{"params": {...}}` 처럼 한 겹 감싸 보내면 **전 필드가 기본값으로 떨어지고 HTTP 200 에 전혀 다른 답**이 나갔다(실측 27배 차이). 호출자는 자기 입력이 버려진 걸 알 길이 없다 — 오답이 성공처럼 보인다. 사람에게도 나쁘지만 **에이전트에게 특히 나쁘다**: 사람은 화면 숫자가 이상하면 멈추지만, 프로그램은 200 을 받으면 그대로 인용한다. 회귀 위험은 켜기 전에 실측했다 — `/simulate` 를 부르는 프론트 4곳의 전송 키 합집합 13개가 **전부 이 모델의 필드**다(모델에만 있는 8개는 무관): SimulatorPage(자동실행) 13 · StrategyDemo 5 · TrustGapPanel 9 · simRequest 10 빌더의 커스텀 전략 본문(`...customSnapshot`)은 이 엔드포인트가 아니라 `/backtest` 로 간다 — 그쪽 모델은 이 변경의 범위가 아니다.
      - `record_provenance` boolean: Capture actual engine/input hashes for a fresh registry-strategy execution; bypass result cache, retain compute limits
      - `strategy` string: Strategy ID
      - `direction` string, nullable: long | short | both (null = use strategy default)
      - `sl_pct` number: Stop Loss %
      - `tp_pct` number: Take Profit %
      - `max_bars` integer: Max holding period (bars)
      - `market_type` 'futures' | 'spot': spot | futures
      - `symbols` string[], nullable: Specific symbols (null = use top_n)
      - `symbol` string, nullable: Single-symbol alias for `symbols`. Auto-mapped (prevents silent ignore of a singular field).
      - `top_n` integer, nullable: Number of coins (null = top 50)
      - `include_stablecoins` boolean: Include stablecoins (USDC, DAI…) in the top_n universe. Default false — ~0 volatility → empty backtests. Ignored when `symbols` is set (explicit picks always honored).
      - `start_date` string, nullable: Backtest start date (YYYY-MM-DD)
      - `end_date` string, nullable: Backtest end date (YYYY-MM-DD)
      - `cutoff_utc` string, date-time, nullable: Only candles closed by this UTC instant; null = current closed hour. Does not freeze historical source revisions.
      - `timeframe` '1H' | '2H' | '4H' | '6H' | '12H' | '1D' | '1W': Candle timeframe: 1H, 2H, 4H, 6H, 12H, 1D, 1W
      - `compounding` boolean: True = reinvest profits (compound), False = fixed position size (simple)
      - `avoid_hours` integer[], nullable: UTC hours to avoid entering trades (0-23). null = use strategy default
      - `avoid_months` integer[], nullable: Months to avoid entering trades (1-12). null = no month filter
      - `min_vol_regime` number, nullable: Minimum ATR ratio (current ATR / 14-period ATR MA) to allow entry. e.g., 0.7 = skip low-volatility periods. null = no filter
      - `btc_trend_filter` boolean: If true, only allow LONG entries when BTC is above its 50-period SMA. Improves LONG edge in bull regimes.
      - `fee_pct` number, nullable: Fee per side (0.0005 = 0.05%). None = use market_type default
      - `leverage` number: Leverage multiplier for PnL calculation
    - `resolved_strategy` string, required
    - `slippage_pct_per_side` object, required
  - `registry_contract_version` 'completed-registry-oos-v1', nullable
  - `oos_split_date` string, nullable
  - `strategy` string, required
  - `direction` string, required
  - `coins_used` integer, required
  - `top_n_requested` integer, nullable: The top_n this run resolved to. null for explicit symbols= requests (no top-N universe).
  - `eligible_universe` integer, nullable: Coins eligible for a top-N universe at run time (stablecoins unless include_stablecoins, non-crypto and liquidity-dead markets excluded), read in the same selection as the coins run. null for symbols= requests (and on /backtest when the strategy is invalid, is_valid=false).
  - `universe_capped` boolean, nullable: eligible_universe < top_n_requested: the request asked for more coins than exist, so the whole eligible universe ran. A cap only — coins lacking data are reported in coins_missing_data. null for symbols= requests.
  - `run_cap` integer, nullable: Q-B131: the per-request limit that actually stopped this run before min(top_n_requested, eligible_universe) coins, or null when none did (coins_used then reaches that number, data gaps aside). See run_cap_reason for which limit.
  - `run_cap_reason` 'indicator_cache' | 'compare_max_coins', nullable: Q-B131: which limit run_cap is. «indicator_cache»: the cached path of /simulate/compare, /simulate/validate and /simulate/optimize computes at most the indicator-cache size per request (no all-coin hold; larger runs are a separate async path, Q-B132). «compare_max_coins»: kept for compatibility — since Q-B55 /simulate/compare rejects a top_n above its limit (COMPARE_MAX_COINS) with HTTP 400, so this cut no longer binds. When both apply, the smaller one — the one that bound — is reported. null when no limit bound.
  - `coins_not_computed` string[]: Q-B131: explicitly requested symbols= that were not run because the per-request compute limit (the indicator-cache size) was reached. Empty when every requested symbol ran. Distinct from coins_missing_data (coins without candles in the window).
  - `data_range` string, required
  - `oos_pct` number, required
  - `oos` OOSResult, required: IS vs OOS comparison.
    - `is_metrics` OOSPeriodMetrics, required: Metrics for one period (IS or OOS).
      - `trades` integer, required
      - `win_rate` number, required
      - `total_return` number, required
      - `profit_factor` number, required
      - `max_dd` number, required
      - `avg_win` number, required
      - `avg_loss` number, required
    - `oos_metrics` OOSPeriodMetrics, required: Metrics for one period (IS or OOS).
      - `trades` integer, required
      - `win_rate` number, required
      - `total_return` number, required
      - `profit_factor` number, required
      - `max_dd` number, required
      - `avg_win` number, required
      - `avg_loss` number, required
    - `degradation_ratio` number, nullable, required
    - `overfit_risk` string, required
  - `monte_carlo` MonteCarloResult, required: Monte Carlo simulation result.
    - `mean_return` number, required
    - `median_return` number, required
    - `std_return` number, required
    - `percentile_5` number, required
    - `percentile_25` number, required
    - `percentile_75` number, required
    - `percentile_95` number, required
    - `worst_case_return` number, required
    - `best_case_return` number, required
    - `worst_case_mdd` number, required
    - `positive_pct` number, required
    - `n_simulations` integer, required
    - `n_trades` integer, required
    - `equity_bands` MCEquityBand[], required
      - `trade_idx` integer, required
      - `p5` number, required
      - `p25` number, required
      - `p50` number, required
      - `p75` number, required
      - `p95` number, required

## Other responses

- `422`: Validation Error

## Changes

- **2026-10-20** `972384919a74`: 4 info
  - added the new optional request property `registry`
  - added the optional property `oos_split_date` to the response with the `200` status
  - added the optional property `registry` to the response with the `200` status
  - added the optional property `registry_contract_version` to the response with the `200` status

[Operation history](https://skmtc.dev/pruviq/apis/pruviq-simulation-api/changes/simulate/validate/post.md)

---

[API](https://skmtc.dev/pruviq/apis/pruviq-simulation-api.md) · [All operations](https://skmtc.dev/pruviq/apis/pruviq-simulation-api/llms.txt) · [OpenAPI document](https://skmtc.dev/pruviq/apis/pruviq-simulation-api/revisions/972384919a74?raw)
