---
title: "Run Backtest"
method: POST
path: "/backtest"
---

# Run Backtest

`POST /backtest`

## 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/backtest`
- Auth: none declared

Run a custom strategy backtest using the ConditionEngine.
This is the core Strategy Builder endpoint.

## Request body

- BacktestRequest: Custom strategy backtest request from Strategy Builder.
  - `name` string
  - `direction` string: short | long. Omitted → SHORT (the documented /backtest default). Unlike /simulate, which resolves an omitted direction from the strategy registry, /backtest has no strategy id to resolve from — send direction explicitly. The response's direction_source says whether it was sent or defaulted (Q-B82).
  - `indicators` object: Indicator configs: {'bb': {'period': 20}, 'ema': {}, ...}
  - `entry` object, required: Entry conditions tree: {'type': 'AND', 'conditions': [...]}
  - `exit_conditions` object, nullable: Optional exit-signal conditions tree (same shape as entry). When met during an open trade, exit early (exit_reason='signal'). null = no signal exit (TP/SL/trailing/timeout only).
  - `avoid_hours` integer[]: UTC hours to avoid (0-23)
  - `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
  - `sl_pct` number: Stop Loss %
  - `tp_pct` number: Take Profit %
  - `tp_source` 'custom_pct' | 'trailing': Exit mode: custom_pct = fixed TP/SL; trailing = trailing stop (tp_pct is the trail distance)
  - `trailing_pct` number, nullable: Trailing stop distance %. If null and tp_source=trailing, uses tp_pct.
  - `max_bars` integer: Max holding period (bars)
  - `top_n` integer, nullable: Number of coins (null = top 50)
  - `symbols` string[], nullable: Specific symbols
  - `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).
  - `symbol` string, nullable: Single-symbol alias for `symbols`. Auto-mapped (prevents silent ignore).
  - `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
  - `fee_pct` number, nullable: Fee per side (0.0005 = 0.05%). None = use market_type default
  - `per_coin_usd` number: USD per coin position (compounding=True: total portfolio capital)
  - `leverage` integer: Leverage multiplier
  - `max_concurrent_positions` integer: Max simultaneous open positions
  - `compounding` boolean: True = reinvest profits (compound), False = fixed position size (simple)

## Response `200`

Successful Response

- BacktestResponse: Custom strategy backtest result.
  - `name` string, required
  - `direction` string, required
  - `executed` boolean: False when the strategy failed validation and nothing was run — every metric is a placeholder (0), NOT a 0-trade result. validation_errors says why. HTTP stays 200 (documented contract).
  - `direction_source` 'request' | 'default_short': «request» = the request sent direction; «default_short» = it was omitted and this run is SHORT (the /backtest default — /simulate would use the strategy registry's direction instead).
  - `sl_pct` number, required
  - `tp_pct` number, required
  - `max_bars` integer, required
  - `indicators_used` string[], required
  - `conditions_count` integer, required
  - `total_trades` integer, required
  - `wins` integer, required
  - `losses` integer, required
  - `win_rate` number, required
  - `total_return_pct` number, required
  - `profit_factor` number, required
  - `avg_win_pct` number, required
  - `avg_loss_pct` number, required
  - `max_drawdown_pct` number, required
  - `max_consecutive_losses` integer, required
  - `tp_count` integer, required
  - `sl_count` integer, required
  - `timeout_count` integer, required
  - `total_fees_pct` number
  - `total_funding_pct` number
  - `sharpe_ratio` number
  - `sortino_ratio` number
  - `calmar_ratio` number
  - `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.
  - `coins_missing_data` integer, nullable: min(top_n_requested, eligible_universe) - (selected coins with at least one candle inside start_date..end_date). Counts selected coins with no complete candle up to the cutoff AND coins whose candles all fall outside the requested window (those stay in coins_used, so this can exceed min(top_n_requested, eligible_universe) - coins_used). null for symbols= requests.
  - `data_range` string, required
  - `equity_curve` EquityPoint[], required
    - `time` string, required
    - `value` number, required
  - `coin_results` CoinResult[]
    - `symbol` string, required
    - `trades` integer, required
    - `wins` integer, required
    - `losses` integer, required
    - `win_rate` number, required
    - `profit_factor` number, required
    - `total_return_pct` number, required
    - `avg_pnl_pct` number, required
    - `tp_count` integer, required
    - `sl_count` integer, required
    - `timeout_count` integer, required
  - `trades` TradeItem[]
    - `symbol` string, required
    - `direction` string, required
    - `entry_time` string, required
    - `exit_time` string, required
    - `entry_price` number
    - `exit_price` number
    - `pnl_pct` number, required
    - `pnl_usd` number
    - `exit_reason` string, required
    - `bars_held` integer, required
    - `mfe_pct` number
    - `mae_pct` number
  - `compounding` boolean
  - `ruin_index` integer, nullable
  - `ruin_trade` RuinTrade: The trade at which capital reached zero (Q-H1218). ruin_index counts trades in realisation (exit) order; a trade table listed in entry order shows a different row at position N, so the trade is named, not only numbered.
    - `symbol` string, required
    - `entry_time` string, required
    - `exit_time` string, required
    - `table_row` integer, nullable
    - `rows_shown` integer, nullable
  - `per_coin_usd` number
  - `leverage` integer
  - `initial_capital_usd` number
  - `total_return_usd` number
  - `total_return_pct_portfolio` number
  - `max_drawdown_usd` number
  - `expectancy` number
  - `recovery_factor` number
  - `payoff_ratio` number
  - `btc_hold_return_pct` number
  - `eth_hold_return_pct` number
  - `btc_hold_curve` EquityPoint[]
    - `time` string, required
    - `value` number, required
  - `eth_hold_curve` EquityPoint[]
    - `time` string, required
    - `value` number, required
  - `var_95` number
  - `cvar_95` number
  - `strategy_grade` string
  - `grade_details` string
  - `edge_p_value` number, nullable
  - `warnings` string[]
  - `walk_forward_consistency` number, nullable
  - `walk_forward_details` string
  - `profit_factor_ci` MetricCI: 지표 하나의 부트스트랩 신뢰구간. ★ `unit` 이 **값으로** 있는 이유 (Q-B29 A, 2026-08-29): 각 지표의 CI 는 «그 헤드라인이 실제로 쓰는 입력» 을 재표집해야 한다. profit_factor ← 거래별 gross P/L → unit="trades" sharpe_ratio ← 일별 수익률 × √365 → unit="daily_returns" 거래를 재표집해 Sharpe CI 를 내면 **화면의 그 Sharpe 가 아니다.** 그런데 틀려 보이지 않는다 — 실측(BTCUSDT rsi<30 long, 176거래): 헤드라인 −0.91, 거래 축 CI −4.78~0.29 로 **헤드라인이 구간 안에 들어간다.** 라벨도 «Sharpe CI», 숫자 모양도 맞고 점추정도 구간 안이다. 축만 다르다. ⇒ 라벨이 스스로 축을 말하게 한다. 소비자는 이 값을 «무엇을 몇 번 재표집한 CI 인가» 문구에 그대로 쓴다(룰4 스코프 라벨).
    - `unit` string, required
    - `point` number, required
    - `lo` number, required
    - `hi` number, required
    - `ci_pct` number, required
    - `n_input` integer, required
    - `n_resamples` integer, required
    - `n_used` integer, required
    - `no_loss_pct` number, nullable
  - `sharpe_ci` MetricCI: 지표 하나의 부트스트랩 신뢰구간. ★ `unit` 이 **값으로** 있는 이유 (Q-B29 A, 2026-08-29): 각 지표의 CI 는 «그 헤드라인이 실제로 쓰는 입력» 을 재표집해야 한다. profit_factor ← 거래별 gross P/L → unit="trades" sharpe_ratio ← 일별 수익률 × √365 → unit="daily_returns" 거래를 재표집해 Sharpe CI 를 내면 **화면의 그 Sharpe 가 아니다.** 그런데 틀려 보이지 않는다 — 실측(BTCUSDT rsi<30 long, 176거래): 헤드라인 −0.91, 거래 축 CI −4.78~0.29 로 **헤드라인이 구간 안에 들어간다.** 라벨도 «Sharpe CI», 숫자 모양도 맞고 점추정도 구간 안이다. 축만 다르다. ⇒ 라벨이 스스로 축을 말하게 한다. 소비자는 이 값을 «무엇을 몇 번 재표집한 CI 인가» 문구에 그대로 쓴다(룰4 스코프 라벨).
    - `unit` string, required
    - `point` number, required
    - `lo` number, required
    - `hi` number, required
    - `ci_pct` number, required
    - `n_input` integer, required
    - `n_resamples` integer, required
    - `n_used` integer, required
    - `no_loss_pct` number, nullable
  - `deflated_sharpe` number, nullable
  - `dsr_haircut_pct` number, nullable
  - `mc_p_value` number, nullable
  - `mc_percentile` number, nullable
  - `jensens_alpha` number, nullable
  - `avg_bars_held` number
  - `median_bars_held` number
  - `positions_skipped` integer
  - `pnl_distribution` integer[]
  - `pnl_buckets` string[]
  - `regime_performance` RegimePerformance
    - `bull` RegimeMetrics, required
      - `trades` integer, required
      - `win_rate` number, required
      - `total_return` number, required
      - `profit_factor` number, required
      - `avg_pnl` number, required
    - `bear` RegimeMetrics, required
      - `trades` integer, required
      - `win_rate` number, required
      - `total_return` number, required
      - `profit_factor` number, required
      - `avg_pnl` number, required
    - `sideways` RegimeMetrics, required
      - `trades` integer, required
      - `win_rate` number, required
      - `total_return` number, required
      - `profit_factor` number, required
      - `avg_pnl` number, required
  - `is_valid` boolean, required
  - `validation_errors` string[]
  - `yearly_stats` YearlyStat[]
    - `year` integer, required
    - `trades` integer, required
    - `wins` integer, required
    - `win_rate` number, required
    - `total_return_pct` number, required
    - `profit_factor` number, required
  - `monthly_stats` MonthlyStat[]
    - `month` string, required
    - `trades` integer, required
    - `wins` integer, required
    - `win_rate` number, required
    - `total_return_pct` number, required
    - `profit_factor` number, required
  - `compute_time_ms` integer
  - `reliability_checks` ReliabilityCheck[], required: 축별 판정 레코드 — 이 응답의 «판정 SSoT». reliability_warning 은 파생물. ⚠ `verdict` 는 세 값이다. `not_evaluated` 를 «통과»로 세면 0거래 응답이 「4개 축 통과」로 읽힌다 — 그 축들은 실행조차 안 됐다.
    - `axis_id` 'trades' | 'ruin' | 'sample' | 'leverage' | 'diversification', required
    - `verdict` 'pass' | 'fail' | 'not_evaluated', required
    - `value` number, nullable
    - `threshold` number, nullable
    - `message` string, nullable
  - `reliability_warning` string, nullable, required: 계약은 build_reliability_warning 한 곳이 소유한다(룰3).

## Other responses

- `422`: Validation Error

---

[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)
