Skip to Content
Environment variables

Environment variables

Declared but not wired up

.env.example documents these three. Each is a reasonable idea; none is implemented.

VariableIntended effectActual status
LUXALGO_PROPFIRMS_URLOverride the prop-firm directory URLNot read. propfirm/firms.py hardcodes DEFAULT_URL. The module imports os but never uses it
BACKTEST_MAX_WORKERSCap concurrent pinets-cli subprocessesNot read. Concurrency comes from max_workers in the sweep YAML config
BACKTEST_RESULTS_DIRChoose where sweep artifacts goNot read. orchestrator/sweep.py writes to a results/<run-id>/ path relative to the project root
Why a declared-but-unread variable is worse than an absent one

Setting one of these changes nothing, which is the mild half. The expensive half is that each one looks like it should work, so you end up debugging the variable instead of the code that would have to read it.

Each verdict in the table above comes down to one question asked of the module that would have to consume the value:

  • LUXALGO_PROPFIRMS_URL would be read in propfirm/firms.py. That module imports os and never uses it, and the URL is the hardcoded DEFAULT_URL constant.
  • BACKTEST_MAX_WORKERS would cap concurrency where the sweep is driven. Concurrency comes from max_workers in the sweep YAML instead, so a run you believe you have capped still executes at the default of 1.
  • BACKTEST_RESULTS_DIR would move the output path. orchestrator/sweep.py writes to results/<run-id>/ relative to the project root, and that is where artifacts land.

The values that do take effect are on the next section.

What actually configures the system

Python — the sweep config file

orchestrator/sweep.py reads its settings from the YAML passed to --config. Worker concurrency is the relevant one here:

max_workers: 1 # default; >1 switches to a ThreadPoolExecutor

The default is 1, i.e. sequential execution. Setting a value above 1 is what actually enables parallel subprocess execution, and it changes the results/<run-id>/config.yaml that gets written alongside the run. Full schema in CLI reference.

The precedence chain for sweep configuration has exactly one link

There is no chain to reason about. The YAML passed to --config is the only source orchestrator/sweep.py reads settings from, and the environment is not consulted at all — so an environment variable cannot override a value, and there is nothing for a setting to out-rank.

What makes that checkable is that the value is recorded with the run. The max_workers you set is the max_workers written into results/<run-id>/config.yaml beside the artifacts, so a finished run reports what it actually used rather than what you believe you asked for.

Python — the prop-firm directory

The URL is a module constant, overridable in code rather than by environment:

# propfirm/firms.py DEFAULT_URL = "https://app.luxalgo.com/api/propfirms/list"

To point it elsewhere, change the constant or monkey-patch it. Cache behaviour is a function argument, not an environment variable:

list_firms(force_refresh=False, ttl_s=86_400) # 24-hour TTL

Python — locating the Node toolchain

pinets_cli/runner.py resolves the Node installation by probing %LOCALAPPDATA%\Tools\node-v20 before falling back to whatever is on PATH. This is what lets the project work with the portable Node that scripts/install.ps1 installs without any configuration.

The probe order is fixed, and the portable Node wins

pinets_cli/runner.py checks %LOCALAPPDATA%\Tools\node-v20 first and only then falls back to whatever is on PATH. The order is the point: a portable Node installed by scripts/install.ps1 is found with no configuration at all, and a PATH Node is the fallback for the machines that never ran the installer.

If both are present the portable copy is the one that runs. That is also why LOCALAPPDATA and PATH are the only non-test variables the codebase reads.

JavaScript — browser smoke tests

The Node smoke tests accept a base URL so a harness can point them at a non-default server:

VariableDefaultUsed by
SMOKE_BASEhttp://127.0.0.1:8080Most __smoke_optimizer_*.cjs suites
TRAD_BASEhttp://localhost:8080/The Phase-F smoke suites
SMOKE_ARTIFACTSunsetDirectory for captured browser artifacts

Example:

SMOKE_BASE=http://127.0.0.2:8765/ node app/__smoke_optimizer_workspace.cjs

.env handling

python-dotenv is a declared dependency, but the codebase does not currently call load_dotenv(). The .gitignore policy is nonetheless real and is the thing that matters if you add a credential later:

PatternIgnored?
.envyes
.env.localyes
.env.*.localyes
.env.exampleno — explicitly un-ignored so the template stays tracked

Setting a real variable

Because nothing calls load_dotenv(), a value must be present in the process environment rather than only in a .env file:

# PowerShell — current session $env:MY_VAR = "value" # PowerShell — persisted for your user account [Environment]::SetEnvironmentVariable("MY_VAR", "value", "User")
Why a `.env` file on its own changes nothing

python-dotenv is a declared dependency, but no entry point calls load_dotenv(). A .env file is therefore never parsed, and a value has to be in the process environment before the process starts.

The two commands above differ only in lifetime. The session-scoped one dies with the window it was typed in. The user-scoped one is written to the account and is there for every new process — which is the behaviour a .env file would have given you.

The .gitignore policy is independent of all this and holds either way: .env, .env.local and .env.*.local are ignored, while .env.example is explicitly un-ignored so the template stays tracked.

Summary

  • .env.example is ahead of the code. Treat it as a wish list.
  • Python configuration lives in the sweep YAML config, not the environment.
  • The only environment variables the code reads today are LOCALAPPDATA/PATH (Node discovery) and the three SMOKE_* / TRAD_BASE test harness variables.
  • .gitignore already protects .env, so adding a real credential later is safe by default.
Last updated on