Files
Aura/docs/specs/fieldtest-0011-ingest.md
T
Brummel 77ad977623 fieldtest: cycle-0011 — 3 examples, 5 findings
Per-cycle fieldtest of the aura-ingest ingestion boundary (issue #7), from the
public interface only. 3 examples (transpose/normalize core; close_stream ->
deterministic harness run with epoch-ns end-to-end; load_m1_window over real
AAPL.US 2006-08 bars + None paths), all green. 0 bugs, 0 friction.

Two spec_gaps named for follow-up (neither a code defect; both filed to the
tracker):
- load_m1_window returns Some(empty M1Columns) — not None — for an in-coverage
  but bar-empty window. The rustdoc's "None if no data in [from_ms,to_ms]" reads
  bar-level but the realized boundary is file-level (it inherits data-server's
  file-skip None). A consumer matching None == "no bars" is wrong for the
  in-coverage-empty case. -> tighten the load_m1_window rustdoc (one line).
- AAPL.US (the carrier/integration-test symbol) has only ~21 M1 bars/month, so
  the SMA-cross never warms and the end-to-end run yields all-zero (degenerate
  but valid) metrics. The boundary is correct (C2 warm-up); the milestone's
  shipped demo data is too thin to show a non-degenerate trace. -> weigh against
  C22's "newcomer sees a populated trace" before the Walking-skeleton close
  (document expected density or name a denser demo symbol/window).
2026-06-04 22:04:48 +02:00

188 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Fieldtest — cycle-0011 — 2026-06-04
**Status:** Draft — awaiting orchestrator triage
**Author:** fieldtester (dispatched by fieldtest skill)
## Scope
Cycle 0011 (Walking-skeleton milestone, #7) shipped a NEW public crate
`aura-ingest` — aura's first real data-source ingestion boundary and the
workspace's external-dependency firewall (it alone links `data-server` and its
transitive `chrono`/`regex`/`zip`). Public surface field-tested:
1. `unix_ms_to_epoch_ns(time_ms: i64) -> Timestamp` — the single C3 unit
normalization (`= ms * 1_000_000`), at this one boundary and nowhere else.
2. `M1Columns` — the OHLCV bar transposed AoS → SoA (C7): public fields
`ts: Vec<Timestamp>`, `open/high/low/close/spread: Vec<f64>`,
`volume: Vec<i64>`; derives Clone + Debug + PartialEq.
3. `transpose_m1(&[M1Parsed]) -> M1Columns` — pure AoS → SoA, normalizing time
at the boundary.
4. `M1Columns::close_stream() -> Vec<(Timestamp, Scalar)>` — the close column as
the engine source-stream shape the SMA-cross sample strategy consumes.
5. `load_m1_window(&Arc<DataServer>, symbol, from_ms, to_ms) -> Option<M1Columns>`
— drains a data-server M1 window and transposes once at the boundary.
Tested from the public interface only: `cargo doc -p aura-ingest` /
`cargo doc -p data-server` rustdoc, the design ledger (C1/C3/C7/C10/C12/C16),
the `data-server` README, and the observable behaviour of the functions —
never `crates/*/src`. The build exercised: `cargo build --workspace` (green from
HEAD), then each fixture via
`cargo run --manifest-path fieldtests/cycle-0011-ingest/Cargo.toml --bin <name>`
so HEAD source is always what runs. The fixture crate is a standalone non-member
crate path-depending on `aura-core/-engine/-std/-ingest` plus the `data-server`
git dep, exactly as a C16 research project that ingests recorded bars would.
## Examples
### fieldtests/cycle-0011-ingest/c0011_1_transpose_core.rs — the pure transpose/normalize core (axis a)
- Drives `unix_ms_to_epoch_ns` on representative values (0, 1 ms, a real 2006-era
Unix-ms), then `transpose_m1` on three hand-built `M1Parsed` bars with distinct
per-field values (so a swapped column would be caught), checks each SoA column
field-wise, index alignment (`ts[i]``close[i]`), `volume` kept i64, purity
(identical bars → equal columns via PartialEq), `close_stream` shape, and the
empty-input edge.
- Why it fits: this is the C3/C7 boundary contract a project author transposing
recorded data exercises, verified from rustdoc alone.
- Outcome: built and ran first try; every assertion held. `ms*1e6` exact,
field-wise transpose correct, volume i64, pure, `close_stream` =
`(normalized ts, close as Scalar::F64)`, empty → empty. Matched expected.
### fieldtests/cycle-0011-ingest/c0011_2_close_stream_run.rs — close_stream → a real engine run (axis b)
- Transposes synthetic OHLCV bars (close rises then reverses), takes
`close_stream()`, feeds it straight into a Harness (SMA(2)/SMA(4) → Sub →
Exposure(4) → SimBroker(1.0) → two shipped `aura_std::Recorder` sinks), runs,
drains, `f64_field` + `summarize`, asserts finite metrics, ≥1 exposure flip,
that recorded equity timestamps equal the ingested normalized bar timestamps
(epoch-ns end to end), and that a disjoint second run is bit-identical (C1).
- Why it fits: this is the C3 → C1 hand-off — the `(Timestamp, Scalar)` shape
`close_stream` returns is exactly `Harness::run`'s source-stream input shape;
the ingestion boundary must connect to the engine with no hand-authored stream.
- Outcome: built and ran first try. equity `[0,0,0,0,1,0,-0.5]`, exposure flips
once at the reversal, `total_pips -0.5`, `max_drawdown 1.5`, recorded ts ==
ingested ts, two runs byte-identical. Matched expected.
### fieldtests/cycle-0011-ingest/c0011_3_load_window_real.rs — load_m1_window against real data (axis c)
- Opens the local `data-server` archive (`DEFAULT_DATA_PATH`), loads the real
AAPL.US 2006-08 M1 window, checks SoA column lengths aligned, every bar ts
normalized to epoch-ns and inside the requested window, `close_stream`
non-decreasing, then runs an end-to-end SMA-cross signal-quality backtest over
the real close bars (deterministic across two runs, C1). Then exercises the
documented `None` paths (far-future window, unknown symbol) and the
in-coverage-but-empty sub-window grey zone. Skips cleanly where
`/mnt/tickdata` is absent.
- Why it fits: the milestone's headline — "a backtest runs over real bars" —
driven exactly as a downstream researcher would, plus the documented `None`
path.
- Outcome: built and ran first try on the real archive. Loaded **21** bars for
2006-08; ts normalized and in-window; `close_stream` ascending; the end-to-end
run was deterministic; far-future window → `None`, unknown symbol → `None`;
an in-coverage empty sub-window → `Some(empty)`. Surfaced two spec_gaps (below).
## Findings
### [working] transpose_m1 / unix_ms_to_epoch_ns / close_stream match the C3/C7 contract from rustdoc alone
- Example: c0011_1.
- What happened: `unix_ms_to_epoch_ns` is exactly `ms * 1_000_000` (checked at 0,
1 ms, and a real 2006 ms = 1_154_390_400_000 → 1_154_390_400_000_000_000 ns);
`transpose_m1` produces field-wise-correct SoA columns with no column swap,
`volume` kept `i64` (C7's one i64 column), `ts[i]` aligned with `close[i]`;
it is pure (identical bars → PartialEq-equal columns, C1); `close_stream`
yields `(normalized ts, Scalar::F64(close))` per bar; empty input → empty
columns and empty stream.
- Why working: the entire pure boundary core is reachable, correct, and
unambiguous from the public rustdoc + ledger (C3/C7/C1) without reading any
implementation source. The public-fielded `M1Columns` + the `M1Parsed` public
fields make a hand-built fixture trivial to author.
- Recommended action: carry-on.
### [working] close_stream feeds Harness::run directly; epoch-ns flows end to end; deterministic
- Example: c0011_2.
- What happened: `close_stream()`'s `Vec<(Timestamp, Scalar)>` is exactly the
shape `Harness::run(vec![stream])` consumes — no adapter. Over a rise-then-
reverse close series the chain warmed, the recorded equity-curve timestamps
equalled the ingested (normalized epoch-ns) bar timestamps verbatim
(normalization survives the whole run loop intact), one exposure sign flip was
registered at the reversal, and a disjoint second run produced a bit-identical
`RunReport.to_json()` (C1).
- Why working: the C3 (ingestion) → C1 (deterministic run) seam — the milestone's
reason to exist — is reachable and correct from the public surface as a
downstream consumer, mirroring the cycle-0007/0009 patterns over real ingested
data rather than a hand-authored stream.
- Recommended action: carry-on.
### [working] load_m1_window over real bars: in-window normalized, ascending, deterministic; far-future & unknown-symbol None paths hold
- Example: c0011_3.
- What happened: against the real archive, `load_m1_window("AAPL.US", 2006-08
window)` returned `Some(M1Columns)` with 21 bars; all SoA columns equal length,
every ts normalized to epoch-ns and inside the requested (×1e6) window,
`close_stream` non-decreasing in ts; the end-to-end backtest over the real
closes ran and was bit-identical across two disjoint runs. A far-future window
for the present symbol → `None`; an unknown symbol → `None`, both as the
rustdoc documents.
- Why working: the real-data path — open archive, load a window, transpose once
at the boundary, run a deterministic backtest — works first try from the public
surface, and the two documented `None` conditions (no file overlaps the window;
unknown symbol) behave as stated.
- Recommended action: carry-on.
### [spec_gap] `Some(empty)` vs `None`: an in-coverage but bar-empty window returns `Some(empty M1Columns)`, not `None`
- Example: c0011_3 (grey-zone block).
- What happened: the `load_m1_window` rustdoc states "`None` if the symbol has no
data in `[from_ms, to_ms]`". Empirically, `None` is **file-level**: it occurs
when no data-server file overlaps the window (e.g. a far-future window — those
files are skipped without I/O) or the symbol is unknown. But a **narrow window
inside a loaded file's date coverage that happens to contain zero bars**
(a single 2006-08-01 day inside the loaded 2006-08 file) returns
`Some(M1Columns { ... empty vecs ... })`, **not** `None`. So a downstream
consumer pattern-matching `None` to mean "no bars here" is wrong for the
in-coverage-empty case; it must additionally check `cols.close.is_empty()`.
- Why spec_gap: "no data in `[from_ms, to_ms]`" reads bar-level on the public
surface, but the realized boundary is file-level (it inherits data-server's own
file-skip `None`). Two readings are equally plausible — "`None` ⇔ zero bars"
(what the wording suggests) vs "`None` ⇔ no overlapping file, else `Some`
(possibly empty)" (what ships). I asserted the **observed (file-level)** reading
in the fixture; a tightening to the bar-level reading would flip that assertion.
- Recommended action: ratify-or-tighten the ledger/rustdoc. Either document
precisely ("`None` only when no file overlaps the window or the symbol is
unknown; an in-coverage window with no bars returns `Some` with empty columns —
check `.close.is_empty()`"), or collapse the empty case into `None` so the
binary `Some`/`None` cleanly means "has bars" / "has none". One rustdoc line on
`load_m1_window` closes it.
### [spec_gap] the only documented real symbol is too sparse for a meaningful signal-quality run over a realistic window
- Example: c0011_3.
- What happened: AAPL.US — the symbol named in the carrier and the gated
integration test — has **21 M1 bars for all of August 2006** and **~100 for the
whole year 2006** (verified via `load_m1_window` over a year/month/day window).
The end-to-end backtest therefore ran but produced **all-zero metrics**
(`total_pips 0, max_drawdown 0, exposure_sign_flips 0`): with any realistic
SMA-cross (even SMA(10)/SMA(30)) the slow SMA never warms over ≤21 bars, so the
signal never fires (correct C2 warm-up / no-look-ahead behaviour — **not a
bug**). The boundary itself is faultless; the data behind the milestone's
shipped example is too thin to demonstrate a *non-degenerate* run.
- Why spec_gap: the milestone promise is "a backtest runs over real bars" — which
is technically met — but a downstream consumer following the documented surface
(this symbol, this archive) cannot reach a *meaningful* signal-quality result,
and nothing on the public surface signals that 21 bars/month is the expected
density (vs a window-bounds bug, a corrupt file, or a normalization error). The
consumer guesses "this is just sparse early data"; "the window is mis-bounded"
is an equally plausible reading from the surface alone. I picked the
sparse-data reading (the bounds were honoured, ts are in-window, the year window
also returns only ~100 bars).
- Recommended action: ratify (document expected per-symbol bar density, or name a
denser default symbol/window for the milestone demo) **or** plan a follow-up
that points the walking-skeleton sample at a real window dense enough to warm a
realistic signal — so "runs over real bars" demonstrably yields a non-degenerate
trace, consistent with C22's "newcomer sees a populated trace". No code change
to `aura-ingest` implied; this is about which data the milestone showcases.
## Recommendation summary
| Finding | Class | Action |
|---|---|---|
| transpose/normalize/close_stream match C3/C7 from rustdoc | working | carry-on |
| close_stream → deterministic engine run, epoch-ns end to end | working | carry-on |
| load_m1_window real bars + documented None paths | working | carry-on |
| `Some(empty)` vs `None` boundary not crisp | spec_gap | ratify / tighten the ledger + rustdoc |
| AAPL.US too sparse for a non-degenerate milestone run | spec_gap | ratify / plan a denser demo symbol+window |