PENROSE
Penrose/Documentation
Back to site

Your first verdict

This walkthrough starts with an empty Penrose clone: no local data, no catalog entries, and no corpus. It installs the package, checks the referee, downloads a keyless public factor series, registers that series, and submits one structured claim. The result is a real research verdict, not a needs_data routing stop.

The commands and output below were verified on 2026-07-24 with Python 3.11.15. The data window is pinned through 2026-05-29 so the artifact hash and expected verdict are reproducible. Ken French data are free and require no API key. Network access is required for the first download.

A verdict reports whether a claim survived Penrose's falsification process. It is never a statement that a strategy is profitable, tradeable, or suitable for capital.

1. Install from the clone

Start in the repository root. Use Python 3.11; that is the version used to verify this walkthrough.

python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e .
python --version

Expected version line:

Python 3.11.15

2. Verify the referee

Run the invariant suite before trusting a verdict:

python scripts/eval_suite.py

The suite prints its individual controls and must finish with:

142/142 passed

Do not continue if it exits nonzero.

3. Initialize an empty catalog

make init creates the ignored, repository-local catalog scaffold. Point this shell explicitly at that directory so there is no ambiguity about which catalog Penrose will load:

make init
export PENROSE_DATA_DIR="$PWD/data"
mkdir -p "$PENROSE_DATA_DIR/files"
cp examples/reference_loader/loader.py "$PENROSE_DATA_DIR/loader.py"

The loader.py copy implements Penrose's catalog contract. catalog.yaml declares metadata; the loader delivers the named CSV as a sorted UTC pandas.Series.

Expected make init output in a fresh clone:

Created /path/to/Penrose/data/catalog.yaml
Add series files under /path/to/Penrose/data/files/ and register them in catalog.yaml.
To use another catalog directory, set PENROSE_DATA_DIR=/path/to/catalog.

4. Download and register free data

This command uses Penrose's keyless Ken French adapter to download the daily U.S. momentum factor. It stores the decimal returns as a local CSV, hashes the exact served bytes, and replaces the empty catalog with one registered series:

PENROSE_SKIP_DOTENV=1 python - <<'PY'
import hashlib
import os
from pathlib import Path

import pandas as pd
import yaml
from penrose.data.vendors import kenfrench

data_dir = Path(os.environ["PENROSE_DATA_DIR"])
result = kenfrench.fetch({
    "dataset": "F-F_Momentum_Factor_daily",
    "column": "Mom",
    "start": "2000-01-01",
    "end": "2026-05-29",
})
if result is None:
    raise SystemExit("Ken French download failed")
series, provenance = result

artifact = data_dir / "files" / "kenfrench_mom_daily.csv"
pd.DataFrame({"date": series.index, "value": series.to_numpy()}).to_csv(
    artifact, index=False
)
sha256 = hashlib.sha256(artifact.read_bytes()).hexdigest()

catalog = {
    "series": {
        "kenfrench_mom_daily": {
            "file": "files/kenfrench_mom_daily.csv",
            "domain": "equity",
            "unit": "decimal_return",
            "frequency": "daily",
            "provenance": provenance,
            "provenance_grade": "as_displayed",
            "agg": "last",
            "artifact_sha256": sha256,
            "description": (
                "Daily U.S. equity momentum factor return from the "
                "Ken French Data Library."
            ),
        }
    }
}
(data_dir / "catalog.yaml").write_text(
    yaml.safe_dump(catalog, sort_keys=False), encoding="utf-8"
)

print(f"registered kenfrench_mom_daily: {len(series)} rows")
print(f"range: {series.index.min().date()} through {series.index.max().date()}")
print(f"provenance: {provenance}")
print(f"artifact_sha256: {sha256}")
PY

Expected output:

registered kenfrench_mom_daily: 6641 rows
range: 2000-01-03 through 2026-05-29
provenance: kenfrench-csv:F-F_Momentum_Factor_daily:Mom
artifact_sha256: 15a64f55260d94063a116c91fed28c33c9aad4e1f7c42d06e760292050d25794

The resulting catalog.yaml is:

series:
  kenfrench_mom_daily:
    file: files/kenfrench_mom_daily.csv
    domain: equity
    unit: decimal_return
    frequency: daily
    provenance: kenfrench-csv:F-F_Momentum_Factor_daily:Mom
    provenance_grade: as_displayed
    agg: last
    artifact_sha256: 15a64f55260d94063a116c91fed28c33c9aad4e1f7c42d06e760292050d25794
    description: Daily U.S. equity momentum factor return from the Ken French Data
      Library.

as_displayed is intentional: this public file is the library's current research series, not a point-in-time vintage.

5. Declare the claim

Save the following as claims.json in the repository root:

[
  {
    "claim_id": "first-verdict-kenfrench-momentum",
    "statement": "The mean of the declared series kenfrench_mom_daily is greater than zero.",
    "mechanism": "Cross-sectional momentum may persist because information diffuses gradually and investors underreact.",
    "scope": "U.S. equity momentum factor returns published by the Ken French Data Library",
    "horizon": "daily observations from 2000-01-01 onward",
    "source_id": "your-first-verdict",
    "source_span": "Test whether the mean of the declared series kenfrench_mom_daily is greater than zero.",
    "claimed_metric_quote": "mean daily return > 0",
    "applicable_strategy_class": "provided_series_statistic",
    "source_type": "external_source",
    "search_cohort_id": "your-first-verdict-singleton",
    "search_denominator": 1,
    "data_provenance": {
      "claim_type": "provided_series_statistic",
      "series": [
        "kenfrench_mom_daily"
      ],
      "provider": "Ken French Data Library",
      "provenance_grade": "as_displayed"
    },
    "sample_period": {
      "start": "2000-01-01",
      "end": "2026-05-29"
    },
    "claim_kind": "relationship"
  }
]

The binding is literal: kenfrench_mom_daily appears in both the catalog and the claim. The provided_series_statistic executor tests the already-constructed factor-return series directly; it does not invent a trading overlay.

6. Run the claim

Use the explicit offline classifier for this keyless structured run:

penrose run --claims claims.json --no-llm --json

--claims skips prose claim extraction. --no-llm also selects P3's documented offline falsifiability fallback, so this guaranteed path requires neither a model key nor a data key. The claim type, data binding, executor, robustness stack, and verdict logic still run.

Observed output:

[penrose] claim workers: resolved=4 requested=4 bound=requested ceilings(cpu=0, ram=0, cap=16)
[penrose] WARNING: real catalog series 'kxfed_signal' unavailable - PENROSE_ALLOW_SYNTHETIC is not set; marking series UNAVAILABLE
[penrose] WARNING: real catalog series 'kxrecssnber_signal' unavailable - PENROSE_ALLOW_SYNTHETIC is not set; marking series UNAVAILABLE
{"principle": null, "source_id": "claims", "status": "complete", "verdicts": [{"claim_id": "first-verdict-kenfrench-momentum", "execution_evidence": "modeled", "kill_reason": "no_oos_edge", "oos_sharpe": 0.01, "resolution": null, "verdict": "kill"}]}

The two warnings concern unrelated built-in macro series; they do not mean kenfrench_mom_daily was missing. The JSON is the result to inspect:

  • status: complete means the run finished.
  • verdict: kill is a real research verdict, not a routing state.
  • kill_reason: no_oos_edge records which configured falsification gate failed.
  • execution_evidence: modeled is a separate evidence axis.
  • principle: null means this single result proposed no corpus principle.

Here, kill means this claim failed the configured process on the pinned sample. It does not say that momentum is always false, and it is not trading advice.

7. Read the recorded result

The compact verdict view should now contain the claim:

penrose verdicts

Observed output:

surface      statistical        execution    syn  claim_id                   statement
---------------------------------------------------------------------------------------------------------------------------------
backtested   kill               modeled      no   first-verdict-kenfrench-mo The mean of the declared series kenfrench_mom_dail

triage is for claims that stopped without a research verdict. This run produced none:

penrose triage

Observed output:

No no-verdict decisions found yet (No traces or decisions found yet to triage).

Launch the read-only local dashboard to inspect the result and its report:

make dash

The verified launch served http://127.0.0.1:8077/. The command stays in the foreground; press Ctrl-C when finished.

8. Stop at P9

Stop here. P8 has emitted a verdict, but promoting any survivor or proposed principle into the approved corpus is P9: a human-only review decision. Do not automate or bypass that gate. This example is a kill, so there is nothing to promote.

Optional: ingest a paper instead

Paper or Markdown ingestion needs a configured OpenAI-compatible model because P2 must extract grounded claims from prose. Put the PENROSE_LLM_API_KEY, PENROSE_LLM_BASE_URL, and PENROSE_LLM_DEFAULT_MODEL values in .env, then use:

set -a
. ./.env
set +a
penrose run --paper path/to/source.pdf --json

This command shape was verified with a local Markdown fixture and a configured model. Its output is source-dependent, so no universal verdict is expected; the verified fixture honestly routed to needs_data. Use the structured path above when you need the reproducible first verdict.

Optional: bring premium data

Penrose can also use licensed premium data, including Databento, when you supply your own entitlement and credentials. Premium adapters are not required for this walkthrough and must not be treated as redistributable merely because they can be loaded locally. Start with Work with data and the repository's adapter guide.

Penrose is a falsification referee for quantitative trading claims. A verdict is evidence about survival under the referee, never a trading instruction.