Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 10 additions & 19 deletions reports/example-diagnostics.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
# Example Diagnostics Report
# Example Diagnostics

This report is generated from the deterministic synthetic demo. It is a smoke-test artifact, not a backtest.
This public demo is intentionally synthetic. It shows the research workflow shape without exposing employer data, private alpha formulas, or tradable production signals.

| Metric | Value | Read |
|---|---:|---|
| observations | 1200 | full synthetic panel used |
| dates | 40 | enough for a small pipeline smoke |
| coverage | 1.000000 | no missing factor/label rows in the demo |
| mean Rank IC | 0.062314 | weak positive rank relationship in synthetic data |
| Rank IC IR | 0.382932 | not enough to call robust |
| positive Rank IC rate | 0.625000 | more positive than negative days |
| mean turnover | 0.686991 | too high for a casual daily signal |
| top-quantile gross return | 0.000743 | positive before costs |
| top-quantile net return, 30 bps | -0.001318 | rejected after turnover costs |
| verdict | `reject_cost_adjusted_return` | correct conservative behavior |
## Current Capability

## What this shows
- `MarketPanelContract` validates a point-in-time market panel and rejects obvious leakage fields.
- `FactorRegistry` records factor lineage, input fields, and point-in-time rules.
- `evaluate_factor` reports rank IC, turnover, gross return, net return, and a rejection verdict when costs overwhelm the demo signal.
- `DataManifest` records dataset shape, identity columns, and known limitations for reproducible handoff.
- `build_walk_forward_splits` and `apply_split` create rolling train/test windows with strict train-before-test ordering.

The package does not stop at an IC number. It carries the result through coverage, turnover, transaction costs, and a simple verdict. The useful feature is not that the toy factor works; it is that the toolkit refuses to dress up a cost-eroded signal.
## Why It Matters

## Next useful artifact

The next version should add a public point-in-time sample dataset and a walk-forward split. That would move the repo from toolkit foundation to a small reproducible research example.
The useful signal for recruiters is not that the synthetic factor is profitable. The useful signal is that the project treats alpha work as a research system: data contract first, leakage checks first, walk-forward evaluation first, and rejection reasons preserved instead of hidden.
6 changes: 6 additions & 0 deletions src/quant_toolkit/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
"""Public clean-room quant research utilities."""

from .contracts import MarketPanelContract
from .manifest import DataManifest
from .metrics import FactorDiagnostics, evaluate_factor
from .registry import FactorRegistry, FactorSpec
from .splits import WalkForwardSplit, apply_split, build_walk_forward_splits

__all__ = [
"DataManifest",
"FactorDiagnostics",
"FactorRegistry",
"FactorSpec",
"MarketPanelContract",
"WalkForwardSplit",
"apply_split",
"build_walk_forward_splits",
"evaluate_factor",
]
50 changes: 50 additions & 0 deletions src/quant_toolkit/manifest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import Iterable

import pandas as pd


@dataclass(frozen=True)
class DataManifest:
source: str
created_at: str
rows: int
columns: tuple[str, ...]
date_col: str
asset_col: str
known_limits: tuple[str, ...] = ()

@classmethod
def from_panel(
cls,
panel: pd.DataFrame,
source: str,
date_col: str = "date",
asset_col: str = "asset",
known_limits: Iterable[str] = (),
) -> "DataManifest":
missing = [col for col in (date_col, asset_col) if col not in panel.columns]
if missing:
raise ValueError(f"missing manifest identity columns: {missing}")
return cls(
source=source,
created_at=pd.Timestamp.utcnow().isoformat(),
rows=int(len(panel)),
columns=tuple(str(col) for col in panel.columns),
date_col=date_col,
asset_col=asset_col,
known_limits=tuple(known_limits),
)

def to_dict(self) -> dict[str, object]:
return {
"source": self.source,
"created_at": self.created_at,
"rows": self.rows,
"columns": list(self.columns),
"date_col": self.date_col,
"asset_col": self.asset_col,
"known_limits": list(self.known_limits),
}
66 changes: 66 additions & 0 deletions src/quant_toolkit/splits.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import Iterable

import pandas as pd


@dataclass(frozen=True)
class WalkForwardSplit:
"""Index-window split with strict train-before-test ordering."""

train_start: pd.Timestamp
train_end: pd.Timestamp
test_start: pd.Timestamp
test_end: pd.Timestamp

def __post_init__(self) -> None:
if not self.train_start <= self.train_end < self.test_start <= self.test_end:
raise ValueError("split dates must satisfy train_start <= train_end < test_start <= test_end")

def as_dict(self) -> dict[str, str]:
return {
"train_start": self.train_start.date().isoformat(),
"train_end": self.train_end.date().isoformat(),
"test_start": self.test_start.date().isoformat(),
"test_end": self.test_end.date().isoformat(),
}


def build_walk_forward_splits(
dates: Iterable[object],
train_window: int,
test_window: int,
step: int | None = None,
) -> list[WalkForwardSplit]:
if train_window <= 0 or test_window <= 0:
raise ValueError("train_window and test_window must be positive")
step = test_window if step is None else step
if step <= 0:
raise ValueError("step must be positive")

unique_dates = pd.Index(pd.to_datetime(list(dates))).drop_duplicates().sort_values()
splits: list[WalkForwardSplit] = []
start = 0
while start + train_window + test_window <= len(unique_dates):
train_start = unique_dates[start]
train_end = unique_dates[start + train_window - 1]
test_start = unique_dates[start + train_window]
test_end = unique_dates[start + train_window + test_window - 1]
splits.append(WalkForwardSplit(train_start, train_end, test_start, test_end))
start += step
return splits


def apply_split(
panel: pd.DataFrame,
split: WalkForwardSplit,
date_col: str = "date",
) -> tuple[pd.DataFrame, pd.DataFrame]:
if date_col not in panel.columns:
raise ValueError(f"missing date column: {date_col}")
dates = pd.to_datetime(panel[date_col])
train_mask = (dates >= split.train_start) & (dates <= split.train_end)
test_mask = (dates >= split.test_start) & (dates <= split.test_end)
Comment on lines +63 to +65
return panel.loc[train_mask].copy(), panel.loc[test_mask].copy()
28 changes: 28 additions & 0 deletions tests/test_manifest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from __future__ import annotations

import unittest

from quant_toolkit.demo import make_synthetic_panel
from quant_toolkit.manifest import DataManifest


class DataManifestTest(unittest.TestCase):
def test_manifest_records_panel_shape_and_limits(self) -> None:
panel = make_synthetic_panel(days=3, assets=2)
manifest = DataManifest.from_panel(panel, source="synthetic-public-demo", known_limits=["not production data"])

payload = manifest.to_dict()

self.assertEqual(payload["rows"], 6)
self.assertIn("momentum_5d", payload["columns"])
self.assertEqual(payload["known_limits"], ["not production data"])

def test_requires_identity_columns(self) -> None:
panel = make_synthetic_panel(days=2, assets=2).drop(columns=["asset"])

with self.assertRaises(ValueError):
DataManifest.from_panel(panel, source="broken")


if __name__ == "__main__":
unittest.main()
36 changes: 36 additions & 0 deletions tests/test_splits.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from __future__ import annotations

import unittest

import pandas as pd

from quant_toolkit.demo import make_synthetic_panel
from quant_toolkit.splits import apply_split, build_walk_forward_splits


class WalkForwardSplitTest(unittest.TestCase):
def test_builds_ordered_non_overlapping_test_windows(self) -> None:
panel = make_synthetic_panel(days=18, assets=3)
splits = build_walk_forward_splits(panel["date"], train_window=8, test_window=3)

self.assertEqual(len(splits), 3)
self.assertLess(splits[0].train_end, splits[0].test_start)
self.assertLess(splits[0].test_end, splits[1].test_start)

def test_apply_split_returns_train_and_test_panels(self) -> None:
panel = make_synthetic_panel(days=12, assets=2)
split = build_walk_forward_splits(panel["date"], train_window=6, test_window=2)[0]

train, test = apply_split(panel, split)

self.assertEqual(train["date"].nunique(), 6)
self.assertEqual(test["date"].nunique(), 2)
self.assertLess(pd.to_datetime(train["date"]).max(), pd.to_datetime(test["date"]).min())

def test_rejects_bad_windows(self) -> None:
with self.assertRaises(ValueError):
build_walk_forward_splits(["2026-01-01"], train_window=0, test_window=1)


if __name__ == "__main__":
unittest.main()
Loading