# DECISIONS Running log of choices the build spec left open, and of anything that failed. Newest phase last. --- ## Phase −1 — Provisioning ### D-001 — The Space lives under the personal namespace, not the org — **decided by the human** `create_repo(..., repo_type="space", space_sdk="gradio")` under `The-Bit-Trading-Company` returns **402 Payment Required**: > Static Spaces are free for everyone, but hosting Gradio and Docker Spaces on > free cpu-basic requires a Team or Enterprise plan for organization > The-Bit-Trading-Company. Verified this is a plan limit, not a token-scope problem: the token has `repo.write` on the org, the dataset repo created fine under the org, and a *static* Space probe under the org succeeded. The org has no paid plan; the owner's personal account is PRO. This hit the "anything requiring payment" flag, so it went to the human, who chose the personal namespace. | Repo | Location | |---|---| | Signal store (dataset) | `The-Bit-Trading-Company/bit-signal-store` | | Backtest Lab (Space) | `Bit-Trading-Company/bit-backtest-lab` | The shared data asset keeps the company namespace; only the app URL changes. The personal account being PRO means ZeroGPU is still available for Phase 4, so no capability is lost. Moving the Space to the org later is a rename, and `src/config.py` reads both ids from env (`BIT_SPACE_REPO`, `BIT_STORE_REPO`). ### D-002 — Python 3.11 toolchain The machine had only Python 3.14, which numba (and therefore vectorbt) does not support. Used `uv` to install 3.11.15 into a local `.venv`. Final resolved stack: vectorbt 1.1.0, numpy 2.4.6, pandas 2.3.3, numba 0.67.0, pyarrow 25.0.1, plotly 6.9.0, gradio 5.49.1. `requirements.txt` pins these for the Space, and the Space is pinned to `python_version: "3.11"` to match. ### D-003 — Writes are one atomic commit, not "manifest last" The spec asks for `CommitScheduler` with "atomic manifest update last". A single `create_commit` carrying the parquet slices *and* the manifest is strictly stronger: readers can never observe a manifest referencing a slice that has not landed yet. `SignalStore.flush()` does that, ordering the manifest as the final operation within the commit. `SignalStore.attach_scheduler()` still provides `CommitScheduler`-based background batching for the running Space, pointed at the same local mirror, so the two paths cannot disagree about what is on disk. ### D-004 — `Custom (code)` preset is present but inert The design's preset list ends with `Custom (code)`, backed by a code editor. The build spec says "No arbitrary code execution from user input anywhere", which is the stronger constraint. The control is rendered, disabled, with an explanation, rather than silently dropped from the design — see `DESIGN_NOTES.md`. ### D-005 — Design source recovered from local files `claude.ai/design/p/00cbd10f…` returned 403 to every unauthenticated fetch and no Chrome instance was connected, so the design was unreachable at first. The human supplied the export locally. The Bit design system tokens and fonts are vendored verbatim into `space/assets/`; `space/DESIGN_NOTES.md` records the extracted layout, copy and component inventory that Phase 3 is built against. ### Provisioning results | Action | Result | |---|---| | `whoami` with token | OK — user `Bit-Trading-Company`, PRO, admin of the org | | Token scope | fine-grained; `repo.write` on org + user | | Create dataset `The-Bit-Trading-Company/bit-signal-store` | **created**, public | | Create Space under org | **failed, 402** — see D-001 | | Create Space `Bit-Trading-Company/bit-backtest-lab` | **created**, gradio, cpu-basic, public | | Space secret `HF_WRITE_TOKEN` | **set** via API; value never logged or committed | | Signal store init | **committed** — `manifest.json` (schema_version 1), `signals/ prices/ comparisons/ runs/`, dataset card | | Hardware `zero-a10g` | not yet requested — deferred to Phase 4 per spec | Token hygiene: the value lives only in the local `key.txt` (gitignored at the repo root) and in the Space secret. It appears in no source file, no committed config, and no log line. `.env.example` documents variable names only. --- ## Phase 0 — Data layer ### D-006 — Providers are config; both primaries failed and the chain absorbed it Two provider-level blocks appeared during seeding, and both were handled by the fallback chain rather than by editing code: - **Binance returns HTTP 451** ("restricted location") from the seeding machine. Crypto OHLCV therefore came from **Coinbase**, the configured fallback. - **Stooq now serves an HTML block page** instead of CSV. `data.py` detects the missing CSV header and advances the chain, so equities ran on **yfinance**. Neither was silently swallowed: every fallback is recorded in the fetch result's notes, and `source` is stored per price row. Recorded here rather than escalated because the spec's flag-to-human bar is provider blocking that persists *after backoff and fallback exhaustion* — the fallbacks succeeded. ### D-007 — Equity gap counts are calendar gaps, not data faults Daily equity series report ~171 "gaps" over three years. Those are weekends and holidays. The gap report measures distance from a continuous bar grid and is surfaced as information; it never fails validation. ## Phase 1 — Engine ### D-008 — Causality is checked by perturbation, in both directions The spec asks for a structural lookahead assertion. `assert_causal` scales the tail of the price frame and requires every earlier output to be bit-identical. Scaling **up only** proved insufficient: a boolean comparison that is already `True` can survive an upward bump unchanged, and a peeking strategy slipped through on one seed. The check now perturbs up *and* down at four probe points, which catches obvious lookahead, centred rolling windows, and full-sample normalisation. ### D-009 — R-multiple when no stop is configured R is P&L over initial risk, and initial risk is the stop distance. With no stop there is no such distance, so the engine falls back to `risk_per_trade_pct` (default 2% of entry notional). It is a documented config field rather than a hidden constant. ### D-010 — Costs are one atomic identity on every trade row Slippage is embedded in the fill price rather than booked as a fee, so it is reconstructed from the *unslipped* reference price (direction-aware: a buy fills high, a sell fills low). This makes `gross − costs = net` exact to 1e-9 on every row, which is what known-answer test 3 asserts. ### D-011 — The holdout applies to every validation mode Originally only `mode="holdout"` reserved one. Walk-forward could then roll its windows across the whole period. Since the design shows rolling windows and an "OOS holdout LAST 6MO" side by side, `holdout_months > 0` now reserves a holdout in every mode except `none`. ### D-012 — An empty segment renders as "—", never 0.00 A short range under walk-forward can produce no out-of-sample window at all. The stat band was reporting `OOS Sharpe 0.00` for that case, which asserts something false. Segments with zero bars now render an em dash, the plan carries an explicit note explaining why no OOS exists, and the overfit verdict counts it as a failed check. ## Phase 2 — Adapters & seed ### D-013 — The v1 seed contains no placeholder slices The spec allows clearly-labelled synthetic placeholders where GPU capacity is short. Batched Chronos-Bolt inference on local MPS ran at roughly **1 ms/step**, so the entire v1 seed was generated for real: **18 slices, 60,375 rows**, across `chronos-bolt-small` and `chronos-bolt-base`, 6 assets, 3 timeframes. `PlaceholderAdapter` and its `PLACEHOLDER` labelling remain in the codebase and are still tested, because contributors on weaker hardware may need them. ### D-014 — Dedup compares the *producible* range, not the requested one A forecast needs a full trailing context window, so the first producible timestamp sits `context_len` bars after the start of a price slice. Comparing the user's typed range against manifest coverage therefore reported already-covered slices as uncovered and would have paid for the same inference twice. Both `scripts/seed_store.py` and `extension.estimate()` now compare the range the request would actually produce. This was caught by the Phase 4 dedup test, not by inspection. ## Phase 5 — Ship ### D-015 — torch pinned to 2.11.0 for ZeroGPU The first deploy landed in `CONFIG_ERROR`: ZeroGPU accepts only torch 2.11.0, 2.10.0, 2.9.1 or 2.8.0, and the build was pinned to the locally-tested 2.13.0. Pinned to **2.11.0**. The local test environment still runs 2.13.0, so the adapter code path is exercised on a newer torch than the Space uses — noted in HANDOFF.md as a residual difference worth a post-deploy check.