diff --git a/src/ml4t_coursework/checks.py b/src/ml4t_coursework/checks.py index 7f4cdd1..881c86e 100644 --- a/src/ml4t_coursework/checks.py +++ b/src/ml4t_coursework/checks.py @@ -57,7 +57,6 @@ class Conformance: delta: str = "" stamped_at: str = "" helper_version: str = "" - unit: str = "" def __str__(self) -> str: head = f"{self.component}: {'conformant' if self.conformant else 'NOT conformant'}" @@ -68,7 +67,6 @@ def __str__(self) -> str: def to_dict(self) -> dict: return { "component": self.component, - "unit": self.unit, "conformant": self.conformant, "checks": [{"name": c.name, "passed": c.passed, "detail": c.detail} for c in self.checks], "reference_delta": self.delta, @@ -181,7 +179,7 @@ def run_invariants(contract, obj) -> list[CheckResult]: def evaluate(contract, obj) -> Conformance: """Run all five checks, stopping the gating ones at the first failure.""" - result = Conformance(component=contract.name, unit=contract.units[0], conformant=True) + result = Conformance(component=contract.name, conformant=True) stages: list[Callable[[], Any]] = [ lambda: [run_interface(contract, obj)], lambda: [run_leakage(contract, obj)], diff --git a/src/ml4t_coursework/components.py b/src/ml4t_coursework/components.py index 5462d7b..e7b00c4 100644 --- a/src/ml4t_coursework/components.py +++ b/src/ml4t_coursework/components.py @@ -196,7 +196,7 @@ def status() -> str: else: failed = [c["name"] for c in meta.get("checks", []) if not c["passed"]] state = f"NOT conformant ({', '.join(failed) or 'unknown'})" - lines.append(f" {name:<20} unit {contract.units[0]:<5} {state}") + lines.append(f" {name:<20} {state}") return "components in your project folder:\n" + "\n".join(lines) diff --git a/src/ml4t_coursework/contracts.py b/src/ml4t_coursework/contracts.py index d5cf287..74f49a2 100644 --- a/src/ml4t_coursework/contracts.py +++ b/src/ml4t_coursework/contracts.py @@ -1,4 +1,11 @@ -"""What a component is, and the registry of the twenty the course asks for. +"""What a component is, and the registry of the twenty the courses ask for. + +A contract says what a component must do. It deliberately does not say which unit writes it: a +unit number belongs to one course's outline, and more than one course installs this package. A +Research to Production student is handed all twenty and never sat the Foundations unit that +writes the baseline, so a number here would appear in their own submission report naming a +lesson they never took. Each course keeps that mapping beside its own units, where it cannot +drift out of step with them - and it did drift, in both directions, while it lived here. One `Contract` per component, declared beside its reference implementation in `reference/`. The contract is authored *from* the reference, which is what guarantees it is passable, and every @@ -23,7 +30,6 @@ class Contract: name: str kind: str - units: tuple[str, ...] summary: str probe: Callable[[Any], Any] interface: Callable[[Any], None] @@ -39,7 +45,7 @@ def __post_init__(self) -> None: raise ValueError(f"{self.name}: kind must be 'callable' or 'config', got {self.kind!r}") def describe(self) -> str: - lines = [f"{self.name} ({self.kind}, written in unit {' and '.join(self.units)})", + lines = [f"{self.name} ({self.kind})", f" {self.summary}", f" interface: {self.interface_detail}", f" leakage probe: {self.leakage_note}"] diff --git a/src/ml4t_coursework/project.py b/src/ml4t_coursework/project.py index b6a6896..e1073d3 100644 --- a/src/ml4t_coursework/project.py +++ b/src/ml4t_coursework/project.py @@ -24,14 +24,26 @@ def on_colab() -> bool: def mount_drive(quiet: bool = True) -> bool: - """Mount Google Drive when running on Colab. A no-op anywhere else.""" + """Mount Google Drive when running on Colab. A no-op anywhere else. + + `google.colab` being importable does not mean a Drive can be mounted. The + published Colab runtime image has the package but no backend to mount + through, so `drive.mount` raises there; a headless or automated session can + also reach a mount it cannot complete. Returning False sends `home` to the + environment override or the home directory, which is the fallback it already + implements. Raising instead would fail before that fallback is ever consulted. + """ try: from google.colab import drive # type: ignore except ImportError: return False - if not (DRIVE_MOUNT / "MyDrive").is_dir(): + if (DRIVE_MOUNT / "MyDrive").is_dir(): + return True + try: drive.mount(str(DRIVE_MOUNT)) - return True + except Exception: + return False + return (DRIVE_MOUNT / "MyDrive").is_dir() def home(create: bool = True) -> Path: diff --git a/src/ml4t_coursework/reference/allocator.py b/src/ml4t_coursework/reference/allocator.py index 8830e7c..756123a 100644 --- a/src/ml4t_coursework/reference/allocator.py +++ b/src/ml4t_coursework/reference/allocator.py @@ -1,6 +1,7 @@ -"""`allocator` - units 8.1 and 8.2. From a position to a weight. +"""`allocator`. From a position to a weight. -8.1 maps the signal to weights. 8.2 tightens the same component with the constraint set, which is +The first unit maps the signal to weights. The second tightens the same component with the +constraint set, which is where the cap and the gross budget stop being implicit. """ @@ -129,7 +130,6 @@ def _finite(obj) -> str: register(Contract( name="allocator", kind="callable", - units=("7.1", "7.2"), summary="Turns positions into weights inside a stated gross budget and per-name cap.", probe=_probe, interface=_interface, diff --git a/src/ml4t_coursework/reference/availability_lag.py b/src/ml4t_coursework/reference/availability_lag.py index 1a1e44e..5ee9142 100644 --- a/src/ml4t_coursework/reference/availability_lag.py +++ b/src/ml4t_coursework/reference/availability_lag.py @@ -1,4 +1,4 @@ -"""`availability_lag` - unit 2.2. The lag before a datum may be used. +"""`availability_lag`. The lag before a datum may be used. This is the decision the rest of the pipeline silently depends on, and its leakage probe is the one check `certification.md` calls the most valuable in the course. @@ -87,7 +87,6 @@ def _leading_gap(obj) -> str: register(Contract( name="availability_lag", kind="callable", - units=("2.2",), summary="Delays every observation by the time it takes to become usable.", probe=_probe, interface=_interface, diff --git a/src/ml4t_coursework/reference/backtest_config.py b/src/ml4t_coursework/reference/backtest_config.py index 31bd15b..d1536d2 100644 --- a/src/ml4t_coursework/reference/backtest_config.py +++ b/src/ml4t_coursework/reference/backtest_config.py @@ -1,4 +1,4 @@ -"""`backtest_config` - unit 7.2. The assumptions the engine makes, said out loud.""" +"""`backtest_config`. The assumptions the engine makes, said out loud.""" from __future__ import annotations @@ -40,7 +40,6 @@ def _rebalance_is_a_period(obj) -> str: register(Contract( name="backtest_config", kind="config", - units=("6.2",), summary="Engine, fill assumption and rebalance frequency, with what the engine cannot answer.", probe=probe, interface=interface_for(FIELDS), diff --git a/src/ml4t_coursework/reference/baseline_strategy.py b/src/ml4t_coursework/reference/baseline_strategy.py index cce6260..761240c 100644 --- a/src/ml4t_coursework/reference/baseline_strategy.py +++ b/src/ml4t_coursework/reference/baseline_strategy.py @@ -1,4 +1,4 @@ -"""`baseline_strategy` - unit 2.5. The auditable non-ML rule everything else must beat.""" +"""`baseline_strategy`. The auditable non-ML rule everything else must beat.""" from __future__ import annotations @@ -81,8 +81,8 @@ def _invested(obj) -> str: require(len(active) > 100, "the baseline is invested", "a rule that holds something over most of the sample", f"positions on only {len(active)} of {len(out)} sessions", - "A baseline that is mostly in cash is a comparison against cash, which 7.3 already " - "makes separately.") + "A baseline that is mostly in cash is a comparison against cash, which the cost " + "model already prices separately.") worst = float((active - 1.0).abs().max()) require(worst < 1e-6, "the baseline is invested", "a fully invested book on every active session", @@ -114,7 +114,6 @@ def _finite(obj) -> str: register(Contract( name="baseline_strategy", kind="callable", - units=("2.5",), summary="An auditable non-ML rule, fixed before any result is seen.", probe=_probe, interface=_interface, diff --git a/src/ml4t_coursework/reference/cost_model.py b/src/ml4t_coursework/reference/cost_model.py index 0a70721..6d9565f 100644 --- a/src/ml4t_coursework/reference/cost_model.py +++ b/src/ml4t_coursework/reference/cost_model.py @@ -1,6 +1,6 @@ -"""`cost_model` - unit 8.3. What trading takes out, before you decide anything survived. +"""`cost_model`. What trading takes out, before you decide anything survived. -The two decisions in 8.3 - the form, and where its parameters come from - are taught rather than +The two decisions here - the form, and where its parameters come from - are taught rather than built, so the shipped form is the one every student uses and what they choose is its parameters. The contract is on the shape any cost model must have, so a student who changes the form still gets checked. @@ -103,7 +103,6 @@ def _finite(obj) -> str: register(Contract( name="cost_model", kind="callable", - units=("7.3",), summary="Charges the strategy for what it traded, in the units the return is measured in.", probe=_probe, interface=_interface, diff --git a/src/ml4t_coursework/reference/data_panel.py b/src/ml4t_coursework/reference/data_panel.py index 6a1ad02..c952709 100644 --- a/src/ml4t_coursework/reference/data_panel.py +++ b/src/ml4t_coursework/reference/data_panel.py @@ -1,4 +1,4 @@ -"""`data_panel` - unit 2.1. The stored price panel, loaded with its close convention stated. +"""`data_panel`. The stored price panel, loaded with its close convention stated. The file on disk is wide, one column per asset, because that is how a price download arrives and how it stores compactly. What the rest of the pipeline works in is long: one row per (date, @@ -129,7 +129,6 @@ def _unbalanced(obj) -> str: register(Contract( name="data_panel", kind="callable", - units=("2.1",), summary="Loads the stored price file and returns it long, one row per (date, asset).", probe=_probe, interface=_interface, diff --git a/src/ml4t_coursework/reference/exit_rule.py b/src/ml4t_coursework/reference/exit_rule.py index 5431e68..4f37344 100644 --- a/src/ml4t_coursework/reference/exit_rule.py +++ b/src/ml4t_coursework/reference/exit_rule.py @@ -1,4 +1,4 @@ -"""`exit_rule` - unit 8.4. Position controls, and the one threshold worth calibrating.""" +"""`exit_rule`. Position controls, and the one threshold worth calibrating.""" from __future__ import annotations @@ -135,7 +135,7 @@ def _fires(obj) -> str: require(closed > 0, "the rule does something", "a position closed somewhere in a 35% fall held throughout", "a rule that never fires even then", - "A threshold so wide it never triggers is not a control; 8.4's own result is that a " + "A threshold so wide it never triggers is not a control; this unit's own result is that a " "stop grid need not identify a stable threshold, which is a different finding from " "never testing one.") return f"{closed} position-days closed by the rule" @@ -144,7 +144,6 @@ def _fires(obj) -> str: register(Contract( name="exit_rule", kind="callable", - units=("7.4",), summary="Closes a position that has breached its control, and leaves it closed.", probe=_probe, interface=_interface, diff --git a/src/ml4t_coursework/reference/features.py b/src/ml4t_coursework/reference/features.py index 0c3e4d7..350b07b 100644 --- a/src/ml4t_coursework/reference/features.py +++ b/src/ml4t_coursework/reference/features.py @@ -1,6 +1,6 @@ -"""`features` - units 5.1 and 5.2. A feature is a hypothesis about a driver. +"""`features`. A feature is a hypothesis about a driver. -5.1 builds one feature that encodes a named driver. 5.2 tightens the same component so the +The first unit builds one feature that encodes a named driver. The second tightens it so the lookback is read against the label horizon and the values are normalized inside each date's cross-section rather than against pooled history. """ @@ -104,7 +104,6 @@ def _finite(obj) -> str: register(Contract( name="features", kind="callable", - units=("4.1", "4.2"), summary="Turns prices into the predictors the model sees, normalized within each date.", probe=_probe, interface=_interface, diff --git a/src/ml4t_coursework/reference/fold_splitter.py b/src/ml4t_coursework/reference/fold_splitter.py index 10e0ae5..2a6c918 100644 --- a/src/ml4t_coursework/reference/fold_splitter.py +++ b/src/ml4t_coursework/reference/fold_splitter.py @@ -1,4 +1,4 @@ -"""`fold_splitter` - unit 3.1. Walk-forward folds with a label buffer.""" +"""`fold_splitter`. Walk-forward folds with a label buffer.""" from __future__ import annotations @@ -102,7 +102,6 @@ def _buffered(obj) -> str: register(Contract( name="fold_splitter", kind="callable", - units=("3.1",), summary="Splits a date index into walk-forward train/validation folds with a label buffer.", probe=_probe, interface=_interface, diff --git a/src/ml4t_coursework/reference/holdout_split.py b/src/ml4t_coursework/reference/holdout_split.py index bc8f250..3070dec 100644 --- a/src/ml4t_coursework/reference/holdout_split.py +++ b/src/ml4t_coursework/reference/holdout_split.py @@ -1,4 +1,4 @@ -"""`holdout_split` - unit 3.2. A dated cutoff, written down and not opened.""" +"""`holdout_split`. A dated cutoff, written down and not opened.""" from __future__ import annotations @@ -72,7 +72,6 @@ def _covers(obj) -> str: register(Contract( name="holdout_split", kind="callable", - units=("3.2",), summary="Seals the last stretch of the sample behind a dated cutoff.", probe=_probe, interface=_interface, diff --git a/src/ml4t_coursework/reference/labeler.py b/src/ml4t_coursework/reference/labeler.py index 1e78661..4278be2 100644 --- a/src/ml4t_coursework/reference/labeler.py +++ b/src/ml4t_coursework/reference/labeler.py @@ -1,6 +1,7 @@ -"""`labeler` - units 3.3 and 3.4. The outcome the model is asked to predict. +"""`labeler`. The outcome the model is asked to predict. -3.3 fixes the horizon and what overlap costs. 3.4 tightens the same component so entry and exit +The first unit fixes the horizon and what overlap costs. The second tightens the same component +so entry and exit are prices actually reachable after the decision, rather than the close of the bar the decision was made on. """ @@ -93,7 +94,7 @@ def _stamped_on_the_decision(obj) -> str: def _executable(obj) -> str: - """3.4's tightening: the entry price is one a decision at t could actually have transacted.""" + """The executable-price tightening: the entry is a price a decision at t could have transacted.""" panel = fixtures.panel() out = obj(panel) close = panel["close"].unstack("asset") @@ -122,7 +123,6 @@ def _finite(obj) -> str: register(Contract( name="labeler", kind="callable", - units=("3.3", "3.4"), summary="Turns prices into the outcome a decision at t is judged on, stamped at t.", probe=_probe, interface=_interface, diff --git a/src/ml4t_coursework/reference/model_gbm.py b/src/ml4t_coursework/reference/model_gbm.py index f2f7fa0..8a870d5 100644 --- a/src/ml4t_coursework/reference/model_gbm.py +++ b/src/ml4t_coursework/reference/model_gbm.py @@ -1,4 +1,4 @@ -"""`model_gbm` - unit 6.3. Gradient boosting, on the same features and the same split as 6.1.""" +"""`model_gbm`. Gradient boosting, on the same features and the same split as the linear model.""" from __future__ import annotations @@ -65,7 +65,7 @@ def _interface(obj) -> None: require(isinstance(out, pd.Series), "interface", "predict to return a Series", f"a {type(out).__name__}") require(out.index.equals(X_va.index), "interface", - "one prediction per row it was asked about, on the same index as 6.1's model, because " + "one prediction per row it was asked about, on the same index as the linear model, because " "the two are compared", f"{len(out)} predictions against {len(X_va)} rows") @@ -103,7 +103,6 @@ def _finite(obj) -> str: register(Contract( name="model_gbm", kind="callable", - units=("5.3",), summary="A gradient-boosted model with capacity chosen rather than defaulted.", probe=_probe, interface=_interface, diff --git a/src/ml4t_coursework/reference/model_linear.py b/src/ml4t_coursework/reference/model_linear.py index 6c78e57..d1dcf13 100644 --- a/src/ml4t_coursework/reference/model_linear.py +++ b/src/ml4t_coursework/reference/model_linear.py @@ -1,4 +1,4 @@ -"""`model_linear` - unit 6.1. The baseline every later result is read against.""" +"""`model_linear`. The baseline every later result is read against.""" from __future__ import annotations @@ -114,7 +114,6 @@ def _finite(obj) -> str: register(Contract( name="model_linear", kind="callable", - units=("5.1",), summary="A regularized linear model: the baseline every later result is read against.", probe=_probe, interface=_interface, diff --git a/src/ml4t_coursework/reference/objective.py b/src/ml4t_coursework/reference/objective.py index 8c3a39c..1cd25f9 100644 --- a/src/ml4t_coursework/reference/objective.py +++ b/src/ml4t_coursework/reference/objective.py @@ -1,4 +1,4 @@ -"""`objective` - unit 1.2. What the strategy is judged on, fixed before any result exists.""" +"""`objective`. What the strategy is judged on, fixed before any result exists.""" from __future__ import annotations @@ -26,7 +26,6 @@ def reference(): register(Contract( name="objective", kind="config", - units=("1.4",), summary="The metric the strategy is judged on and the cost tier it is judged under.", probe=probe, interface=interface_for(FIELDS), diff --git a/src/ml4t_coursework/reference/preprocessor.py b/src/ml4t_coursework/reference/preprocessor.py index b3fc101..9954907 100644 --- a/src/ml4t_coursework/reference/preprocessor.py +++ b/src/ml4t_coursework/reference/preprocessor.py @@ -1,4 +1,4 @@ -"""`preprocessor` - unit 4.4. Winsorize, scale, impute, all fitted on training rows only.""" +"""`preprocessor`. Winsorize, scale, impute, all fitted on training rows only.""" from __future__ import annotations @@ -136,7 +136,6 @@ def _finite(obj) -> str: register(Contract( name="preprocessor", kind="callable", - units=("3.6",), summary="Winsorizes, scales and imputes, with every parameter learned on training rows.", probe=_probe, interface=_interface, diff --git a/src/ml4t_coursework/reference/quality_gates.py b/src/ml4t_coursework/reference/quality_gates.py index 788f1a1..827c38e 100644 --- a/src/ml4t_coursework/reference/quality_gates.py +++ b/src/ml4t_coursework/reference/quality_gates.py @@ -1,4 +1,4 @@ -"""`quality_gates` - unit 2.3. What a breach does, and to which bar.""" +"""`quality_gates`. What a breach does, and to which bar.""" from __future__ import annotations @@ -130,7 +130,6 @@ def _keeps_good(obj) -> str: register(Contract( name="quality_gates", kind="callable", - units=("2.3",), summary="Voids bars that fail a stated quality gate, and reports which gate caught what.", probe=_probe, interface=_interface, diff --git a/src/ml4t_coursework/reference/signal.py b/src/ml4t_coursework/reference/signal.py index 6f1709b..3b8e916 100644 --- a/src/ml4t_coursework/reference/signal.py +++ b/src/ml4t_coursework/reference/signal.py @@ -1,4 +1,4 @@ -"""`signal` - unit 7.1. From a score to a position: which names, and which way.""" +"""`signal`. From a score to a position: which names, and which way.""" from __future__ import annotations @@ -91,7 +91,6 @@ def _two_sided(obj) -> str: register(Contract( name="signal", kind="callable", - units=("6.1",), summary="Turns model scores into which names to hold and which way.", probe=_probe, interface=_interface, diff --git a/src/ml4t_coursework/reference/strategy_spec.py b/src/ml4t_coursework/reference/strategy_spec.py index 4ddc94c..e18036c 100644 --- a/src/ml4t_coursework/reference/strategy_spec.py +++ b/src/ml4t_coursework/reference/strategy_spec.py @@ -1,4 +1,4 @@ -"""`strategy_spec` - unit 1.1. The family you are in, and the mechanism you are claiming.""" +"""`strategy_spec`. The family you are in, and the mechanism you are claiming.""" from __future__ import annotations @@ -26,7 +26,6 @@ def reference(): register(Contract( name="strategy_spec", kind="config", - units=("1.3",), summary="The strategy family and the economic mechanism the return is claimed to come from.", probe=probe, interface=interface_for(FIELDS), diff --git a/src/ml4t_coursework/reference/task_form.py b/src/ml4t_coursework/reference/task_form.py index 0e48a00..04e234b 100644 --- a/src/ml4t_coursework/reference/task_form.py +++ b/src/ml4t_coursework/reference/task_form.py @@ -1,4 +1,4 @@ -"""`task_form` - unit 4.3. The form the model predicts in. +"""`task_form`. The form the model predicts in. The course fixes regression, and the argument is that the form is fixed by what the allocation rule consumes rather than by which form a model predicts best. So the contract does not require @@ -89,7 +89,6 @@ def _finite(obj) -> str: register(Contract( name="task_form", kind="callable", - units=("3.5",), summary="Puts the label in the form the model predicts, keeping the order the allocator reads.", probe=_probe, interface=_interface, diff --git a/src/ml4t_coursework/reference/universe.py b/src/ml4t_coursework/reference/universe.py index f3d9682..04c692c 100644 --- a/src/ml4t_coursework/reference/universe.py +++ b/src/ml4t_coursework/reference/universe.py @@ -1,4 +1,4 @@ -"""`universe` - unit 2.4. Who you may trade, decided with what was knowable at the time.""" +"""`universe`. Who you may trade, decided with what was knowable at the time.""" from __future__ import annotations @@ -99,7 +99,6 @@ def _sorted_and_unique(obj) -> str: register(Contract( name="universe", kind="callable", - units=("2.4",), summary="Decides which assets may be traded on a date, using only what was known then.", probe=_probe, interface=_interface, diff --git a/src/ml4t_coursework/report.py b/src/ml4t_coursework/report.py index 0dac28c..86a5245 100644 --- a/src/ml4t_coursework/report.py +++ b/src/ml4t_coursework/report.py @@ -27,7 +27,6 @@ def report(answers: dict[str, str] | None = None, quiet: bool = False) -> dict: for name in sorted(known): meta = _meta(name) stamps[name] = { - "unit": known[name].units[0], "written": meta is not None, "conformant": bool(meta and meta.get("conformant")), "stamped_at": (meta or {}).get("stamped_at"), diff --git a/src/ml4t_coursework/runner.py b/src/ml4t_coursework/runner.py index 1ddab78..3c0e6af 100644 --- a/src/ml4t_coursework/runner.py +++ b/src/ml4t_coursework/runner.py @@ -12,7 +12,8 @@ The window matters more than it looks. Every run reports the development window unless it is asked for the holdout, because a runner that reported the holdout on every assembly run would -spend it dozens of times, which is the one thing unit 8.1 says not to do. +spend it dozens of times, which is the one thing the unit on multiple testing and the +promotion standard says not to do. """ from __future__ import annotations @@ -114,44 +115,44 @@ def run(spec: MarketSpec, *, stage: str, strategy: str = "pipeline", get = _resolve(components) bars = spec.bars_per_year - # 6.2 how the backtest executes, including how often the book is allowed to change + # How the backtest executes, including how often the book is allowed to change config = get("backtest_config") rebalance = int(config["rebalance"]) - # 2.1 the stored panel, long + # The stored panel, long source = spec.acquire() panel = get("data_panel")(str(source)) - # 2.2 nothing is usable the moment it is stamped + # Nothing is usable the moment it is stamped panel = get("availability_lag")(panel) panel = panel[panel["close"].notna()] - # 2.3 a breach voids the bar + # A breach voids the bar panel, breaches = get("quality_gates")(panel) panel = panel[panel["close"].notna()] sessions = panel.index.get_level_values("date").unique() assets = sorted(panel.index.get_level_values("asset").unique()) - # 2.4 who may be traded, decided with what was knowable then + # Who may be traded, decided with what was knowable then eligible = _eligibility(get("universe"), panel, sessions, assets, rebalance) - # 3.2 the holdout is sealed here and read only when this run is asked for it + # The holdout is sealed here and read only when this run is asked for it development, holdout = get("holdout_split")(sessions) reported = holdout if window == "holdout" else development if strategy == "baseline": - # 2.5 the auditable non-ML rule everything else must beat + # The auditable non-ML rule everything else must beat book = get("baseline_strategy")(panel).reindex(index=sessions, columns=assets).fillna(0.0) else: - # 3.3/3.4 the outcome a decision is judged on, and 3.5 the form the model predicts + # The outcome a decision is judged on, and the form the model predicts it in target = get("task_form")(get("labeler")(panel)) - # 4.1/4.2 the predictors + # The predictors X = get("features")(panel) shared = X.index.intersection(target.index) X, y = X.loc[shared].sort_index(), target.loc[shared].sort_index() - # 3.1 folds exist so a student can select on them; this run reports, it does not select + # Folds exist so a student can select on them; this run reports, it does not select get("fold_splitter")(development) # Fitting stops a label horizon before the development window ends, so no training label @@ -161,25 +162,25 @@ def run(spec: MarketSpec, *, stage: str, strategy: str = "pipeline", fit_until = development[max(len(development) - purge, 0) - 1] is_fit = dates <= fit_until - # 3.6 every parameter it uses is learned on the fitting rows and only there + # Every parameter it uses is learned on the fitting rows and only there prep = get("preprocessor")() prep.fit(X[is_fit]) Z = prep.transform(X) - # 5.1 the model + # The model model = get("model_linear")() model.fit(Z[is_fit], y[is_fit]) scores = pd.Series(model.predict(Z), index=Z.index).unstack("asset") scores = scores.reindex(index=sessions, columns=assets).where(eligible) - # 6.1 scores into positions, 7.1/7.2 positions into a book + # Scores into positions, positions into a book book = get("allocator")(get("signal")(scores)).reindex( index=sessions, columns=assets).fillna(0.0) - # The book is only allowed to change on the cadence 6.2 declared. Without this the - # allocator re-solves every bar and the run reports the turnover of a different strategy. + # The book is only allowed to change on the cadence backtest_config declared. Without + # this the allocator re-solves every bar and the run reports another strategy's turnover. on_schedule = pd.Series(book.index.isin(sessions[::rebalance]), index=book.index) book = book.where(on_schedule, axis=0).ffill().fillna(0.0) - # 7.4 position controls, which act between rebalances and so come after the cadence + # Position controls, which act between rebalances and so come after the cadence book = get("exit_rule")(book, panel) book = book.where(eligible, 0.0) @@ -190,7 +191,7 @@ def run(spec: MarketSpec, *, stage: str, strategy: str = "pipeline", trades = book.diff() trades.iloc[0] = book.iloc[0] - # 7.3 what trading takes out + # What trading takes out costs = get("cost_model")(trades).reindex(sessions).fillna(0.0) net = (gross - costs).rename("return") diff --git a/tests/test_helper.py b/tests/test_helper.py index cd00e9c..95666e1 100644 --- a/tests/test_helper.py +++ b/tests/test_helper.py @@ -148,9 +148,17 @@ def test_status_lists_every_component_the_course_asks_for(): assert "conformant" in text and "not written yet" in text -def test_the_stamp_records_which_unit_wrote_it(): +def test_the_stamp_records_what_a_later_unit_needs_to_reload_it(): + """The stamp carries no unit number: which unit writes a component is a course's fact, and + this package serves more than one course.""" save_component("fold_splitter", _good_splitter(), quiet=True) - assert _meta("fold_splitter")["unit"] == "3.1" + meta = _meta("fold_splitter") + assert meta["component"] == "fold_splitter" + assert meta["conformant"] is True + assert meta["symbol"] == "fold_splitter" + assert meta["file"].endswith(".py") + assert meta["stamped_at"] + assert "unit" not in meta def test_the_packaged_fingerprint_matches_the_repository_copy(): diff --git a/tests/test_multi_course.py b/tests/test_multi_course.py index edb070e..4b879bd 100644 --- a/tests/test_multi_course.py +++ b/tests/test_multi_course.py @@ -90,7 +90,7 @@ def test_discovery_reaches_a_package_outside_this_one(monkeypatch, tmp_path): "from ml4t_coursework.contracts import Contract, register\n" "def reference():\n" " return lambda x: x\n" - "register(Contract(name='their_component', kind='callable', units=('1.1',),\n" + "register(Contract(name='their_component', kind='callable',\n" " summary='theirs', probe=lambda obj: obj('x'),\n" " interface=lambda obj: None, reference=reference))\n") monkeypatch.syspath_prepend(str(tmp_path)) @@ -112,5 +112,51 @@ def _reference(): _reference.__module__ = "somewhere_else" with pytest.raises(ValueError, match="somewhere_else"): contracts.register(contracts.Contract( - name="fold_splitter", kind="callable", units=("1.1",), summary="clash", + name="fold_splitter", kind="callable", summary="clash", probe=existing.probe, interface=existing.interface, reference=_reference)) + + +def _fake_colab(monkeypatch, mount): + """Install a `google.colab.drive` whose `mount` behaves as `mount` says.""" + import types + + google = types.ModuleType("google") + colab = types.ModuleType("google.colab") + drive = types.ModuleType("google.colab.drive") + drive.mount = mount + colab.drive = drive + google.colab = colab + for name, module in [ + ("google", google), + ("google.colab", colab), + ("google.colab.drive", drive), + ]: + monkeypatch.setitem(sys.modules, name, module) + + +def test_setup_falls_back_when_the_drive_cannot_actually_be_mounted(monkeypatch, tmp_path): + """The published Colab image ships `google.colab` with no backend to mount through, so + `drive.mount` raises there. Raising out of `setup` fails before `home` ever consults the + environment override that exists for exactly this case.""" + + def refuse(_path): + raise NotImplementedError("mounting is not supported in this environment") + + _fake_colab(monkeypatch, refuse) + monkeypatch.setenv("ML4T_FOUNDATIONS_HOME", str(tmp_path / "project")) + use("foundations") + + assert project.mount_drive() is False + assert project.setup(quiet=True) == tmp_path / "project" + + +def test_a_mount_that_reports_success_without_producing_mydrive_is_not_a_mount( + monkeypatch, tmp_path +): + """`drive.mount` returning normally is not evidence the folder is there; the caller decides + where the project lives from whether MyDrive exists.""" + _fake_colab(monkeypatch, lambda _path: None) + monkeypatch.setenv("ML4T_FOUNDATIONS_HOME", str(tmp_path / "project")) + use("foundations") + + assert project.mount_drive() is False diff --git a/tests/test_references.py b/tests/test_references.py index 1204993..eb83181 100644 --- a/tests/test_references.py +++ b/tests/test_references.py @@ -39,7 +39,6 @@ def test_every_contract_has_the_five_checks(name): def test_the_registry_covers_the_component_list(): assert len(ALL) == 20, f"expected the 20 components of the component list, found {len(ALL)}" for name, contract in contracts.load_all().items(): - assert contract.units, f"{name} does not say which unit writes it" assert contract.summary.endswith("."), f"{name}'s summary is not a sentence"