Skip to Content
CLI reference

CLI reference

The Python track is a set of small scripts, each with an argparse surface. Run them from the project root — the strategy and dataset paths in configs resolve relative to the repo root, not your current directory.

All paths in a config resolve against the repository root via (_ROOT / cfg.strategy).resolve(). Running a script from a subdirectory is therefore safe.

What the rebase covers, and what it does not

Two keys, and only two: strategy and dataset. Each is joined to the repository root before the cell runs — (_ROOT / cfg.strategy).resolve() — so a relative path in the YAML is never relative to you. dataset: null has nothing to rebase and falls through to the synthetic fixture.

examples/sweep_quick.yaml writes app/strategies/ema_cross.pine and tests/fixtures/btc_1h_sample.json that way for exactly this reason. The keys you might expect to be paths are not: asset is a label copied into each cell and timeframes is the outer loop of cell expansion, and neither is passed to the CLI.

Pipeline at a glance

examples/sweep_quick.yaml │ ▼ sweep.py ──────────────► results/<run-id>/ │ ├─ cell_<i>.json │ ├─ trades_<i>.csv │ ├─ config.yaml │ └─ summary.parquet ▼ rank.py ────► ranked.csv report.py ──► report.html derive_r_multiples.py ──► trades.csv ──► propfirm/evaluate.py ──► result.json ▲ propfirm/sweep.py ────────────────────────────────────┘

orchestrator/sweep.py

Run a multi-cell backtest sweep.

FlagTypeDefaultNotes
--configstr—Required. Path to the sweep YAML
--run-idstrNoneOptional run-id label; otherwise generated
--dry-runflagFalsePrint the expanded grid and exit without running

orchestrator/hyperopt.py

Bayesian hyperparameter search over a Pine strategy.

FlagTypeDefaultNotes
--configstr—Required. Path to the hyperopt YAML
--trialsint50Trial budget
--studystrconfig file stemStudy name
--resumeflagFalseAppend to an existing study DB
--run-idstrNoneRun label
--dry-runflagFalsePrint the resolved config and exit

orchestrator/rank.py

Rank a sweep summary.parquet.

FlagTypeDefaultNotes
--summarystr—Required
--metricstrprofit_factorNo choices= — any column in the parquet is accepted
--min-tradesint1Drop rows with fewer trades
--min-win-ratefloatNoneOptional floor
--max-drawdown-ltfloatNoneOptional ceiling
--require-positive-plflagFalseKeep only positive-P&L rows
--topint10Rows written
--outstr—Required. Output CSV

orchestrator/report.py

Build a self-contained HTML report.

FlagTypeDefaultNotes
--summarystr—Required
--outstr—Required
--metricstrprofit_factorApplies to the top-10 bar and titles; the histogram is hardcoded to profit_factor

orchestrator/derive_r_multiples.py

Phase-2 bridge: per-cell trades → an evaluate.py-shaped CSV.

FlagTypeDefaultNotes
--run-dirstr—Required. A results/<run-id> directory containing trades_*.csv
--cells-fromstrNoneCSV whose cell_id column selects which cells to include
--risk-pctfloat0.01Assumed per-trade risk as a fraction of capital (0.01 = 1%)
--outstr—Required. Writes open_time, close_time, side, r_multiple
How the two tracks meet

One CSV with a fixed header. Phase-1 writes a trades_<i>.csv per cell into results/<run-id>/. derive_r_multiples.py reads that run directory, keeps every cell or only the ones named by the cell_id column of --cells-from, and writes open_time, close_time, side, r_multiple — which is exactly the column set propfirm/evaluate.py takes as --trades and propfirm/sweep.py takes as --from-run. The bridge is a file format, not an API, so a column that drifts fails at the boundary rather than halfway through.

Nothing but the trade list crosses it. The Phase-1 concerns — asset, timeframes, the ranking metric — stay on the Python track. The Phase-2 scripts take a run directory, a trades file and their own config, and nothing else; the r_multiple they read is already computed from --risk-pct.

propfirm/evaluate.py

Prop-firm evaluation for a single challenge.

FlagTypeDefaultNotes
--tradesstr—Required. CSV with open_time, close_time, side, r_multiple
--firmstrftmo
--challengestr—Required. Challenge id from the live directory
--riskfloat1.0Risk per trade, percent of balance
--seedint42Monte Carlo seed
--outstr—Required. Output JSON

propfirm/sweep.py

Fan per-cell Phase-1 trades across (firm, challenge) targets.

FlagTypeDefaultNotes
--from-runstr—Required. Phase-1 run dir containing trades_<id>.csv
--configstr—Required. YAML with firms / challenges / risk / seed
--cells-fromstrNoneCSV with a cell_id column; default is every cell that has trades
--out-dirstr<from-run>/propfirmOutput directory
--dry-runflagFalsePrint the expanded job grid and exit

propfirm/rank.py

Rank a prop-firm sweep summary.

FlagTypeDefaultNotes
--summarystr—Required
--metricstrpassProbabilityConstrained to passProbability, evTotal, fundedProbability, win_rate, avg_win_R, avg_loss_R, n_trades
--min-tradesint1Drop jobs whose source cell has fewer trades
--require-positive-evflagFalseKeep only evTotal > 0
--topint10
--outstr—Required

propfirm/report.py

Build a self-contained HTML report for a prop-firm sweep.

FlagTypeDefaultNotes
--summarystr—Required
--outstr—Required
--metricstrpassProbabilityConstrained to passProbability, evTotal, fundedProbability

Sweep config schema

orchestrator/sweep.py reads a flat, top-level mapping. There is no sweep: wrapper, and unknown keys are silently ignored.

KeyTypeDefaultMeaning
assetstrBTCUSDTLabel copied into each cell. Not sent to the CLI when dataset is set
timeframeslist[60]Outer loop of cell expansion. Also not passed to the CLI
parameter_gridname → list{}Full cross product over sorted(keys)
strategystrapp/strategies/ema_cross.pineResolved against the repo root
datasetstr | nullNonePinned OHLCV file. null ⇒ synthetic fixture
synth_nint1000Candle count for the synthetic feed
synth_seedint42Seed for the synthetic feed
initial_capitalfloat10000.0
commission_pctfloat0.1Percent per side, charged on entry and exit notional
slippage_pctfloat0.05Percent, applied adversely on both sides
ranking_metricstrprofit_factorSets the cell ranking, the CLI top-3 sort and the report bar chart
robustness_filtermapping{}Parsed and written back — never applied by sweep.py
max_workersint11 = serial loop; >1 = ThreadPoolExecutor

Cell count is len(timeframes) × Π len(values).

Why the cell count is the check that matters

The product is over the inputs that were actually rewritten into the script, not over the lists in the YAML. Rewriting is a textual match that needs its single space — a declaration padded for alignment is skipped and keeps whatever value the source declares. The dimension collapses to one value instead of erroring, which is why the arithmetic still balances and nothing warns you.

The cross product runs over sorted(keys), so declaration order in the YAML is irrelevant, and timeframes is the outer loop. examples/sweep_extended.yaml is the same schema at 432 cells (6 fast × 6 slow × 3 atr × 4 timeframes) with max_workers: 4.

A real example

examples/sweep_quick.yaml expands to 6 cells (fast_len 2 × slow_len 2 × atr_len 1 × timeframes 1):

asset: BTCUSDT timeframes: [60] strategy: app/strategies/ema_cross.pine dataset: tests/fixtures/btc_1h_sample.json synth_n: 1000 synth_seed: 42 parameter_grid: fast_len: [9, 21] slow_len: [21, 55] atr_len: [14] initial_capital: 10000 commission_pct: 0.10 slippage_pct: 0.05 ranking_metric: profit_factor robustness_filter: min_trades: 5 max_workers: 1

Grid rewriting matches input.int(…) / input.float(…) with a single space before the parenthesis. A declaration padded to align (atr_len = input.int(14, …)) is not rewritten, which is why atr_len contributes only one cell in the example above rather than three.

examples/sweep_extended.yaml is the same schema at 432 cells (6 fast × 6 slow × 3 atr × 4 timeframes) with max_workers: 4.

Hyperopt config schema

orchestrator/hyperopt.py reads exactly three top-level blocks: hyperopt:, sweep: and search_space:.

hyperopt:

KeyDefaultNotes
assetBTCUSDT
timeframes["60"]Only timeframes[0] is used
strategyapp/strategies/multi_signal.pine
datasetNone
search_space_path<config>.space.yamlOnly consulted when search_space: is absent
objectivesharpeOne of sharpe, profit_factor, net_profit_pct, calmar, expectancy, sortino_proxy. An unknown name silently falls back to sharpe
directionmaximizeSets the Optuna study direction only — the score function always maximises, so minimize does not actually minimise
train_frac0.7Train cutoff at round(bars × train_frac)
initial_capital10000.0Falls back to sweep:
commission_pct0.1Falls back to sweep:
slippage_pct0.05Falls back to sweep:
min_train_trades5Applied to the train partition
min_validate_trades3Winner-eligibility gate
min_validate_scoreNoneNo threshold unless the key is present
n_jobs1→ study.optimize(n_jobs=…). YAML-only, no CLI flag
seed42Sampler seed
samplertpetpe | random | cmaes. Unknown ⇒ KeyError
prunermedianmedian | hyperband | none. Unknown ⇒ KeyError
pruner_warmup_steps0Also gates whether pruning is reported at all
pruner_interval_steps0Passed as max(value, 1)

sweep:

Only three keys are read, and only as fallbacks: initial_capital, commission_pct, slippage_pct. Everything else is ignored.

search_space:

Each entry is a spec:

FormFieldsOptuna call
type: intlow, high (required), step (default 1)suggest_int
type: floatlow, high (required), step, log (default false)suggest_float
type: catchoices (required)suggest_categorical
type: bool—suggest_categorical(name, [False, True]) — a 2-way choice, not suggest_bool
anything else—ValueError

Parameters are sampled in sorted() order, so declaration order in the file does not matter.

Prop-firm sweep config

Flat, single level:

KeyTypeDefaultValidation
risk_pctfloat1.0A fraction — 0.01 means 1%
seedint42
max_workersint10 coerces to 1
firmslist[]Each entry needs firm and a non-empty challenges list, or ValueError

Running the pipeline

# 1. sweep (add --dry-run to print the grid first) python orchestrator/sweep.py --config examples/sweep_quick.yaml # 2. rank and report python orchestrator/rank.py --summary results/<run-id>/summary.parquet \ --metric profit_factor --out ranked.csv python orchestrator/report.py --summary results/<run-id>/summary.parquet --out report.html # 3. hyperparameter search python orchestrator/hyperopt.py --config examples/hyperopt_quick.yaml --trials 5 # 4. bridge to R-multiples, then evaluate against a challenge python orchestrator/derive_r_multiples.py --run-dir results/<run-id> --out trades.csv python propfirm/evaluate.py --trades trades.csv --firm ftmo --challenge <id> --out result.json

Start with —dry-run. It prints the expanded grid without executing anything, which is the cheapest way to catch a cell-count surprise.

What each --dry-run prints, and what it cannot tell you

Three payloads behind one flag name. orchestrator/sweep.py prints the expanded grid, orchestrator/hyperopt.py prints the resolved config, and propfirm/sweep.py prints the expanded job grid. npm run verify is the same idea as config parsing without executing.

All of them print and exit before a cell runs, so the printed count is the entire check: compare it against len(timeframes) × Π len(values). A count that is too small means an input declaration was padded and skipped. Nothing on that path can tell you a cell would have succeeded, or what it would have produced.

Modules with no CLI

propfirm/firms.py is a library — import list_firms() / list_challenges() rather than running it. pinets_cli/runner.py and pinets_cli/dataset.py are likewise imported by the scripts above.

Fixture generator

tests/fixtures/generate.py regenerates the offline test fixture:

FlagTypeDefault
--nint1000
--seedint42
--startstr2024-01-01T00:00:00Z
--outstrtests/fixtures/btc_1h_sample.json
Last updated on