fieldtest: milestone runway — real-data seam, 4 examples, 6 findings (gate green)
Milestone-close fieldtest for "Runway — real-data ergonomics & honesty hardening". Four curated end-to-end scenarios derived top-down from the milestone promise, run as a downstream consumer against real GER40/EURUSD M1 bars (public interface only; no crates/*/src read): 1. per-instrument pip honesty (#22) — GER40 index pip 1.0 AND EURUSD forex pip 0.0001 both honest end-to-end; AAPL.US/FOOBAR un-vetted refused exit 2, no stdout leak 2. real GER40 M1 open via open_ohlc from aura-ingest ALONE (#80/#81/#92) — no data-server dep, 53173 real bars, C4 O/H/L/C order, ts-window inverse 3. multi-tap ts-join (#93) — two taps at different cadences, 27744 rows, 0 by-ts mismatches; warm-up None rows where a positional zip would corrupt 4. run-registry coherence (#73/#82) — list/rank retired exit 2, families/ family live, two-store Registry read-back + rustdoc Findings: 4 working, 1 friction, 1 spec_gap. - friction: Cell exposes only from_i64/from_f64/... while the side-by-side Scalar is a tagged enum with public I64(..)/F64(..) variants. The natural param-authoring entry point is Scalar (Cell is the hot-path buffer carrier), so the wrong reach is a self-correcting compile snag — ergonomic only. Filed as a follow-up idea. - spec_gap: unknown family id exits 0 empty — ratified into the C18 note in the preceding commit (treat-as-empty contract). Gate verdict: GREEN. All seven axes mapped to a covering scenario and exercised on real bars; both finding classifications and the one material coverage gap were adversarially re-verified. That gap — #22's FX-vetted pip face, untested by the original scenario set — was probed (EURUSD -> sim-optimal(pip_size=0.0001), FX-scaled, not index-mis-scaled) and folded into scenario 1, so the pip promise is demonstrated on both positive faces. Every scenario re-run independently on HEAD before commit.
This commit is contained in:
@@ -0,0 +1,173 @@
|
|||||||
|
# Fieldtest — milestone Runway (real-data ergonomics & honesty hardening) — 2026-06-18
|
||||||
|
|
||||||
|
**Status:** Draft — awaiting orchestrator triage
|
||||||
|
**Author:** fieldtester (dispatched by fieldtest skill, milestone-close gate)
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Milestone-close fieldtest for **Runway — real-data ergonomics & honesty
|
||||||
|
hardening** (commit range `aedaa5d..HEAD`, HEAD `0d185fb`). The milestone's
|
||||||
|
promise: *a downstream consumer can run an honest real-data backtest end to
|
||||||
|
end* — open the real GER40 M1 archive via the canonical opener from
|
||||||
|
`aura-ingest` alone, run a strategy against real bars, get a PnL scaled by the
|
||||||
|
**correct per-instrument pip** (an index must not be mis-scaled as forex), be
|
||||||
|
**refused with a clear diagnostic** when the instrument has no vetted pip spec,
|
||||||
|
join multi-tap sink traces honestly on timestamp, and find the run-registry
|
||||||
|
surface coherent (the dead-end `runs list`/`rank` retired; `runs families`/
|
||||||
|
`family` live; the two-store Registry discoverable from the public docs).
|
||||||
|
|
||||||
|
Scenarios were derived **top-down from the promise**, not as a union of the
|
||||||
|
per-cycle axes. Public interface only: design ledger (`docs/design/INDEX.md`),
|
||||||
|
`docs/glossary.md`, `docs/project-layout.md`, `cargo doc` rustdoc, `git log`,
|
||||||
|
`CLAUDE.md`. No `crates/*/src` (incl. the engine's own
|
||||||
|
`crates/aura-ingest/examples/`, which sits under the forbidden code root) was
|
||||||
|
read. Built and ran HEAD from the working tree throughout: workspace built with
|
||||||
|
`cargo build --workspace`; every fixture/CLI run via `cargo run` so HEAD source
|
||||||
|
is always what executes. The two Rust fixtures path-dep the engine crates as a
|
||||||
|
real C16 project would; the CLI scenarios invoke the `aura` binary directly.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### `fieldtests/milestone-runway/out_rw_1_pip_honesty.txt` — per-instrument pip honesty (#22)
|
||||||
|
- CLI invocations of `aura run --real` on a vetted index and on un-vetted
|
||||||
|
instruments.
|
||||||
|
- Fits the scope: this is the headline honesty fix — "symbol data carries no
|
||||||
|
pip metadata, so a real run guessed". A consumer must get the right pip on a
|
||||||
|
vetted symbol and a refusal on an un-vetted one.
|
||||||
|
- Outcome: ran. `GER40` → exit 0, manifest broker `sim-optimal(pip_size=1)`
|
||||||
|
(correct index point, not the FX `0.0001`), `total_pips=977.38`. `AAPL.US`
|
||||||
|
(un-specced) and `FOOBAR` (un-specced + no archive) → **exit 2**, stderr
|
||||||
|
diagnostic, **no stdout leak**. The refusal fires at the pip lookup *before*
|
||||||
|
data access (FOOBAR, which has no archive, gets the pip-refusal diagnostic,
|
||||||
|
not a data-not-found one). Matched expected.
|
||||||
|
|
||||||
|
### `fieldtests/milestone-runway/rw_2_open_ohlc_real.rs` — real GER40 M1 ingestion via the canonical opener (#80/#81/#92)
|
||||||
|
- A consumer opens the real GER40 archive with `aura_ingest::{default_data_server,
|
||||||
|
DEFAULT_DATA_PATH, open_ohlc}`, wires the four OHLC sources into a 4-source-role
|
||||||
|
harness (SMA-cross-on-close → Exposure → SimBroker(pip 1.0)), and runs to
|
||||||
|
completion over real bars.
|
||||||
|
- Fits the scope: it exercises the "build a real-data source from `aura-ingest`
|
||||||
|
alone" promise (#81), the canonical 4-source merge order (#92), and the
|
||||||
|
timestamp-window open (#80) in one realistic task.
|
||||||
|
- Outcome: built (after one fix — see the `Cell` constructor friction) and ran.
|
||||||
|
`open_ohlc` returned exactly 4 sources O/H/L/C, each reporting `bounds()`
|
||||||
|
without materializing; 53173 real bars streamed on all four taps; OHLC
|
||||||
|
ordering (`low<=open<=high`) held on **53173/53173** bars; close-driven equity
|
||||||
|
produced (`+314.46` pips at pip 1.0). The crate has **no `data-server`
|
||||||
|
dependency** — the #81 crutch the prior milestone-18 fixture needed is gone.
|
||||||
|
Matched expected.
|
||||||
|
|
||||||
|
### `fieldtests/milestone-runway/rw_3_multitap_ts_join.rs` — multi-tap trace honesty (#93)
|
||||||
|
- A consumer records two taps (exposure + cumulative equity) off the same real
|
||||||
|
GER40 run — the taps fire at **different cadences** — and fuses them with
|
||||||
|
`aura_engine::join_on_ts` / `JoinedRow`.
|
||||||
|
- Fits the scope: directly the #93 promise — "join multi-tap sink traces
|
||||||
|
honestly on timestamp, not by index."
|
||||||
|
- Outcome: built and ran. Exposure fired 27725×, equity 27744× (differ by 19);
|
||||||
|
`join_on_ts` produced exactly one `JoinedRow` per spine entry (27744); a
|
||||||
|
by-timestamp cross-check of every `Some`/`None` side against the exposure
|
||||||
|
stream found **0 mismatches**; the first rows are the warm-up bars where
|
||||||
|
exposure had not yet fired (`None`) — precisely the cardinality-misalignment a
|
||||||
|
positional zip would corrupt. Matched expected.
|
||||||
|
|
||||||
|
### `fieldtests/milestone-runway/out_rw_4_runs_coherence.txt` — run-registry coherence (#73/#82)
|
||||||
|
- CLI invocations confirming `runs list`/`rank` are retired and
|
||||||
|
`runs families`/`runs family … rank` work, plus a rustdoc check of the
|
||||||
|
two-store `Registry::open`.
|
||||||
|
- Fits the scope: the registry-honesty half of the promise.
|
||||||
|
- Outcome: ran. `runs list` and `runs rank` → **exit 2** with the usage line
|
||||||
|
(clean diagnostic; both absent from the usage string). `runs families` →
|
||||||
|
exit 0 listing the family; `runs family <id> rank total_pips` → exit 0 with
|
||||||
|
ranked members. The `Registry::open` rustdoc documents the two
|
||||||
|
directory-co-located stores and the per-directory-isolation consequence (#82).
|
||||||
|
Matched expected — with one edge observation (bogus family id, below).
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
### [working] Per-instrument pip honesty is correct and refuses cleanly
|
||||||
|
- Example: `out_rw_1_pip_honesty.txt`.
|
||||||
|
- Vetted `GER40` emits `sim-optimal(pip_size=1)` in the manifest (index point,
|
||||||
|
not the FX `0.0001`); un-vetted `AAPL.US`/`FOOBAR` exit 2 with a stderr
|
||||||
|
diagnostic that names the symbol and the fix ("add it to the instrument
|
||||||
|
table") and **no stdout leak**. The refusal gates at the lookup before any
|
||||||
|
data access. This is the milestone's headline honesty win, working end to end.
|
||||||
|
- Recommended action: carry on.
|
||||||
|
|
||||||
|
### [working] A real-data source builds from `aura-ingest` alone — the `data-server` crutch is gone
|
||||||
|
- Example: `rw_2_open_ohlc_real.rs` + its Cargo.toml.
|
||||||
|
- `default_data_server()`, `DEFAULT_DATA_PATH`, `DataServer` (re-export), and
|
||||||
|
`open_ohlc` are all reachable from `aura-ingest`; the fixture crate carries
|
||||||
|
**no** `data-server` dependency. The prior milestone-18 fixture had to pull
|
||||||
|
`data-server` as a git dep because `M1FieldSource::open` wanted
|
||||||
|
`&Arc<DataServer>` and aura-ingest re-exported neither it nor the path. #81
|
||||||
|
closes that. `open_ohlc` vouches for the C4 O/H/L/C order so the consumer
|
||||||
|
never spells it out (#92); 53173 real bars streamed to completion.
|
||||||
|
- Recommended action: carry on.
|
||||||
|
|
||||||
|
### [working] Multi-tap traces fuse honestly on timestamp
|
||||||
|
- Example: `rw_3_multitap_ts_join.rs`.
|
||||||
|
- Two real-data taps at genuinely different cadences (Δ19 rows) join to exactly
|
||||||
|
one `JoinedRow` per spine entry, every `Some`/`None` side aligning to the
|
||||||
|
source stream **by timestamp** (0 by-ts mismatches over 27744 rows). The
|
||||||
|
Option-per-side semantics surfaced the warm-up `None` rows exactly where a
|
||||||
|
zip-by-index would have silently shifted every later row. The `Recorder`
|
||||||
|
rustdoc states the cadence rule ("join on the recorded timestamp") at the
|
||||||
|
point of use, so a consumer is steered to `join_on_ts` rather than a zip.
|
||||||
|
- Recommended action: carry on.
|
||||||
|
|
||||||
|
### [working] Run-registry surface is coherent; two-store behaviour is documented
|
||||||
|
- Example: `out_rw_4_runs_coherence.txt`.
|
||||||
|
- `runs list`/`rank` are gone from the usage string and exit 2; `runs families`/
|
||||||
|
`runs family … rank` work and emit JSON. `Registry::open`'s rustdoc documents
|
||||||
|
the two directory-co-located stores (`runs.jsonl` + the fixed-name
|
||||||
|
`families.jsonl` sibling) and the per-directory (not per-filename) isolation
|
||||||
|
consequence at the bind point (#82) — exactly the "discoverable from the
|
||||||
|
public docs" half of the promise.
|
||||||
|
- Recommended action: carry on.
|
||||||
|
|
||||||
|
### [friction] Hand-authoring a param-cell vector needs `Cell::from_i64`, not the obvious `Cell::I64`
|
||||||
|
- Example: `rw_2_open_ohlc_real.rs` (first compile attempt).
|
||||||
|
- A consumer bootstrapping a harness with literal params naturally reaches for
|
||||||
|
`Cell::I64(3)` / `Cell::F64(1.0)` (mirroring `Scalar::I64` / `Scalar::F64`,
|
||||||
|
which ARE tuple-style and used right beside it for grid values). `Cell` instead
|
||||||
|
exposes only `from_i64` / `from_f64` / `from_bool` / `from_ts` constructors, so
|
||||||
|
the first build failed with `no associated item 'I64' found for struct Cell`.
|
||||||
|
The compiler's suggestion was excellent (it listed `from_i64` etc.), so the fix
|
||||||
|
was a 5-second one — but the asymmetry with `Scalar`'s tuple variants is a real
|
||||||
|
papercut for hand-authored params (the prior fieldtests sidestepped it by only
|
||||||
|
ever feeding `Cell`s that came pre-built out of a `GridSpace`). Friction, not a
|
||||||
|
bug: the task completed and the diagnostic was clean.
|
||||||
|
- Recommended action: plan a tidy iteration — either add `Cell::I64`/`F64`/etc.
|
||||||
|
tuple-style constructors (or `From<i64>`/`From<f64>` impls) to match `Scalar`,
|
||||||
|
or document the `from_*` constructors on `Cell`'s rustdoc as the param-authoring
|
||||||
|
entry point. (Low priority; ergonomic only.)
|
||||||
|
|
||||||
|
### [spec_gap] `runs family <bogus-id>` exits 0 with empty output, not a "no such family" diagnostic
|
||||||
|
- Example: `out_rw_4_runs_coherence.txt`, final block.
|
||||||
|
- `aura runs family nope-99` (a family id that does not exist) prints nothing and
|
||||||
|
exits **0**. The retired commands exit 2 with a usage line, and a real consumer
|
||||||
|
who typos a family id would reasonably expect a non-zero exit or a "no such
|
||||||
|
family `nope-99`" message rather than silent success that looks
|
||||||
|
indistinguishable from "a real but empty family". The top-level usage string,
|
||||||
|
the ledger (C18 realization note), and the glossary do not constrain what an
|
||||||
|
unknown-but-well-formed family id should do, so I cannot tell whether exit 0 +
|
||||||
|
empty is the intended contract (treat-as-empty, mirroring how a missing store
|
||||||
|
is `Ok(vec![])`) or an oversight. Two readings are equally plausible:
|
||||||
|
(a) **treat-as-empty** — consistent with `Registry::load` treating a missing
|
||||||
|
file as an empty registry, so an unknown id is "a family with zero members";
|
||||||
|
(b) **not-found is an error** — consistent with the retired commands and with
|
||||||
|
typo-safety. I picked no behaviour to assert in a fixture; I recorded the
|
||||||
|
observation.
|
||||||
|
- Recommended action: ratify the intended behaviour into the C18 realization
|
||||||
|
note (and the CLI usage), or tighten it to a non-zero exit + diagnostic if (b).
|
||||||
|
|
||||||
|
## Recommendation summary
|
||||||
|
|
||||||
|
| Finding | Class | Action |
|
||||||
|
|---|---|---|
|
||||||
|
| Per-instrument pip honesty (vetted pip + refusal) | working | carry-on |
|
||||||
|
| Real-data source builds from `aura-ingest` alone (`open_ohlc`, re-exports) | working | carry-on |
|
||||||
|
| Multi-tap traces fuse on timestamp (`join_on_ts`) | working | carry-on |
|
||||||
|
| Run-registry coherence + two-store rustdoc | working | carry-on |
|
||||||
|
| `Cell` lacks tuple-style constructors (asymmetry with `Scalar`) | friction | plan (tidy iteration) |
|
||||||
|
| `runs family <bogus-id>` → exit 0, empty | spec_gap | ratify / tighten the design ledger |
|
||||||
Generated
+1010
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,52 @@
|
|||||||
|
# Standalone downstream-consumer crate for the MILESTONE fieldtest of "Runway —
|
||||||
|
# real-data ergonomics & honesty hardening".
|
||||||
|
#
|
||||||
|
# This is the milestone-close fieldtest: it exercises the honesty-hardened
|
||||||
|
# real-data ingestion seam end to end, the way a downstream researcher would —
|
||||||
|
# open the real GER40 M1 archive via the canonical opener from `aura-ingest`
|
||||||
|
# ALONE (the #81 promise: a real-data source builds without naming the external
|
||||||
|
# `data-server` crate), run a strategy against real bars, and fuse multi-tap sink
|
||||||
|
# traces honestly on timestamp (#93).
|
||||||
|
#
|
||||||
|
# Like the per-cycle fixtures, this is NOT a member of the aura workspace — it
|
||||||
|
# path-deps the engine crates exactly as a real C16 research project would, then
|
||||||
|
# drives the PUBLIC surface ONLY (design ledger + glossary + `cargo doc` rustdoc;
|
||||||
|
# no crates/*/src was read). NB the Runway milestone's whole point is that a
|
||||||
|
# real-data consumer no longer reaches for `data-server` directly: there is NO
|
||||||
|
# `data-server` dependency in this manifest — `aura-ingest` re-exports DataServer
|
||||||
|
# + DEFAULT_DATA_PATH + default_data_server() + open_ohlc() now (#81/#92). The
|
||||||
|
# prior milestone-18 fixture had to pull `data-server` as a git dep; this one
|
||||||
|
# proves that crutch is gone.
|
||||||
|
#
|
||||||
|
# Built/run via, e.g.:
|
||||||
|
# cargo run --manifest-path fieldtests/milestone-runway/Cargo.toml --bin rw_2_open_ohlc_real
|
||||||
|
# so HEAD source is always what runs.
|
||||||
|
#
|
||||||
|
# Empty [workspace] table: marks this fixture crate as its OWN workspace root.
|
||||||
|
[workspace]
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "runway-fieldtest"
|
||||||
|
version = "0.0.0"
|
||||||
|
edition = "2024"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
aura-core = { path = "../../crates/aura-core" }
|
||||||
|
aura-engine = { path = "../../crates/aura-engine" }
|
||||||
|
aura-ingest = { path = "../../crates/aura-ingest" }
|
||||||
|
aura-std = { path = "../../crates/aura-std" }
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "rw_2_open_ohlc_real"
|
||||||
|
path = "rw_2_open_ohlc_real.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "rw_3_multitap_ts_join"
|
||||||
|
path = "rw_3_multitap_ts_join.rs"
|
||||||
|
|
||||||
|
# Scenario 1 (per-instrument pip honesty) and scenario 4 (run-registry coherence)
|
||||||
|
# are exercised directly through the `aura` binary, since the canonical consumer
|
||||||
|
# form for a CLI tool is the CLI invocation itself, not a Rust program. Their
|
||||||
|
# transcripts live in out_rw_1_pip_honesty.txt / out_rw_4_runs_coherence.txt;
|
||||||
|
# there is no [[bin]] for either.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Runway milestone fieldtest — scenario 1: per-instrument pip honesty (#22)
|
||||||
|
#
|
||||||
|
# The task: a downstream researcher runs a real backtest on a VETTED index and
|
||||||
|
# on an UN-VETTED instrument, and must get an honest pip on the first, a clean
|
||||||
|
# REFUSAL (non-zero exit, no silent mis-scaled output) on the second.
|
||||||
|
#
|
||||||
|
# Consumer form: the canonical CLI invocation itself (aura run --real).
|
||||||
|
# Built/run via 'cargo run' so HEAD is what runs.
|
||||||
|
|
||||||
|
$ aura run --real GER40 --from 1420070400000 --to 1422748800000
|
||||||
|
{"manifest":{"commit":"0d185fb4c3d2be3002760dbb9d9a8a2ceed795e0","params":[["sma_fast",{"I64":2}],["sma_slow",{"I64":4}],["exposure_scale",{"F64":0.5}]],"window":[1420156800000000000,1422648780000000000],"seed":0,"broker":"sim-optimal(pip_size=1)"},"metrics":{"total_pips":977.3799999994587,"max_drawdown":654.9300000000931,"exposure_sign_flips":8155}}
|
||||||
|
exit=0
|
||||||
|
|
||||||
|
# ^ VETTED INDEX: broker label reports sim-optimal(pip_size=1) — the correct
|
||||||
|
# index point, NOT the FX default 0.0001. Honest PnL.
|
||||||
|
|
||||||
|
$ aura run --real EURUSD --from 1420070400000 --to 1422748800000
|
||||||
|
{"manifest":{"commit":"0d185fb4c3d2be3002760dbb9d9a8a2ceed795e0","params":[["sma_fast",{"I64":2}],["sma_slow",{"I64":4}],["exposure_scale",{"F64":0.5}]],"window":[1420156800000000000,1422654780000000000],"seed":0,"broker":"sim-optimal(pip_size=0.0001)"},"metrics":{"total_pips":-0.8768415000001664,"max_drawdown":0.9156020000001746,"exposure_sign_flips":8503}}
|
||||||
|
exit=0
|
||||||
|
|
||||||
|
# ^ VETTED FX MAJOR: broker label reports sim-optimal(pip_size=0.0001) — the
|
||||||
|
# correct forex pip, NOT the index point 1. total_pips=-0.8768 is FX-scaled
|
||||||
|
# (sub-pip on this window), NOT a mis-scaled index number (~977 on GER40 above
|
||||||
|
# over the same window). The pip promise's SECOND positive face holds: index
|
||||||
|
# point AND forex pip both demonstrated end-to-end, each at its own scale.
|
||||||
|
|
||||||
|
$ aura run --real AAPL.US --from 1420070400000 --to 1422748800000 # un-vetted (instrument_spec -> None)
|
||||||
|
aura: no vetted pip/instrument spec for symbol 'AAPL.US' — refusing to run a real instrument with a guessed pip (add it to the instrument table)
|
||||||
|
exit=2 (stdout above is the stderr diagnostic only — no mis-scaled stdout leak)
|
||||||
|
|
||||||
|
$ aura run --real FOOBAR --from 1420070400000 --to 1422748800000 # un-vetted AND no archive
|
||||||
|
aura: no vetted pip/instrument spec for symbol 'FOOBAR' — refusing to run a real instrument with a guessed pip (add it to the instrument table)
|
||||||
|
exit=2 (pip-refusal fires at the lookup, BEFORE any data access)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
default_data_server() over DEFAULT_DATA_PATH = /mnt/tickdata/Pepperstone
|
||||||
|
instrument_spec(GER40) = pip_size 1
|
||||||
|
open_ohlc(GER40, [1420070400000..1425254400000]) -> 4 sources (expect 4: O,H,L,C)
|
||||||
|
source[0] open: bounds Some((1420070400000, 1425254400000))
|
||||||
|
source[1] high: bounds Some((1420070400000, 1425254400000))
|
||||||
|
source[2] low: bounds Some((1420070400000, 1425254400000))
|
||||||
|
source[3] close: bounds Some((1420070400000, 1425254400000))
|
||||||
|
|
||||||
|
harness param-space slots:
|
||||||
|
sma_cross.fast.length : I64
|
||||||
|
sma_cross.slow.length : I64
|
||||||
|
exposure.scale : F64
|
||||||
|
|
||||||
|
streamed to completion: open=53173 high=53173 low=53173 equity=53173 bars recorded
|
||||||
|
OHLC ordering (low<=open<=high) holds on 53173/53173 real bars
|
||||||
|
|
||||||
|
GER40 close SMA-cross over real bars: 53173 equity points, final cumulative = 314.46 pips (pip_size=1.0)
|
||||||
|
real bar window actually streamed: [1420156800000..1425254400000] (unix-ms)
|
||||||
|
|
||||||
|
OK: real GER40 OHLC opened from aura-ingest alone -> 4 sources, C4 order -> ran to completion.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
two taps, DIFFERENT cadences: exposure fired 27725 times, equity fired 27744 times
|
||||||
|
-> lengths differ by 19 rows; zip-by-index would silently misalign every row past the first divergence
|
||||||
|
|
||||||
|
join_on_ts: spine(equity)=27744 rows -> 27744 JoinedRows (exactly one per spine entry)
|
||||||
|
exposure side: present(Some)=27725, absent(None)=19, by-ts cross-check mismatches=0
|
||||||
|
|
||||||
|
first 6 joined rows (equity spine | exposure side):
|
||||||
|
ts=1420156800000 equity=0.0000 exposure=None (had not fired at this ts)
|
||||||
|
ts=1420156919999 equity=0.0000 exposure=None (had not fired at this ts)
|
||||||
|
ts=1420156980000 equity=0.0000 exposure=None (had not fired at this ts)
|
||||||
|
ts=1420157040000 equity=0.0000 exposure=None (had not fired at this ts)
|
||||||
|
ts=1420157099999 equity=0.0000 exposure=None (had not fired at this ts)
|
||||||
|
ts=1420157160000 equity=0.0000 exposure=None (had not fired at this ts)
|
||||||
|
|
||||||
|
OK: two real-data taps at different cadences fused HONESTLY on timestamp (27725 aligned, 19 side-absent), never by index.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Runway milestone fieldtest — scenario 4: run-registry coherence (#73/#82)
|
||||||
|
#
|
||||||
|
# The task: a downstream researcher confirms the dead-end 'runs list'/'rank'
|
||||||
|
# surface is RETIRED (clean diagnostic, non-zero exit) while 'runs families'/
|
||||||
|
# 'runs family' still work, and the two-store Registry behaviour is discoverable
|
||||||
|
# from the public docs.
|
||||||
|
#
|
||||||
|
# Consumer form: the canonical CLI invocation itself. Built/run via 'cargo run'.
|
||||||
|
|
||||||
|
## top-level usage (note: no 'runs list', no 'runs rank')
|
||||||
|
$ aura
|
||||||
|
aura: usage: aura run [--macd] | aura run --real <SYMBOL> [--from <ms>] [--to <ms>] | aura graph | aura sweep [--name <n>] | aura mc [--name <n>] | aura walkforward [--name <n>] | aura runs families | aura runs family <id> [rank <metric>]
|
||||||
|
exit=2
|
||||||
|
|
||||||
|
## RETIRED surface — must refuse
|
||||||
|
$ aura runs list
|
||||||
|
aura: usage: aura run [--macd] | aura run --real <SYMBOL> [--from <ms>] [--to <ms>] | aura graph | aura sweep [--name <n>] | aura mc [--name <n>] | aura walkforward [--name <n>] | aura runs families | aura runs family <id> [rank <metric>]
|
||||||
|
exit=2
|
||||||
|
|
||||||
|
$ aura runs rank total_pips
|
||||||
|
aura: usage: aura run [--macd] | aura run --real <SYMBOL> [--from <ms>] [--to <ms>] | aura graph | aura sweep [--name <n>] | aura mc [--name <n>] | aura walkforward [--name <n>] | aura runs families | aura runs family <id> [rank <metric>]
|
||||||
|
exit=2
|
||||||
|
|
||||||
|
## LIVE surface — must work (after a sweep persisted family 'ft-coherence-0')
|
||||||
|
$ aura runs families
|
||||||
|
{"family_id":"ft-coherence-0","kind":"Sweep","members":4}
|
||||||
|
exit=0
|
||||||
|
|
||||||
|
$ aura runs family ft-coherence-0 rank total_pips # (first member shown; full list in run output)
|
||||||
|
{"manifest":{"commit":"0d185fb4c3d2be3002760dbb9d9a8a2ceed795e0","params":[["signals.trend.fast.length",{"I64":2}],["signals.trend.slow.length",{"I64":4}],["signals.momentum.fast.length",{"I64":2}],["signals.momentum.slow.length",{"I64":4}],["signals.momentum.signal.length",{"I64":3}],["signals.blend.weights[0]",{"F64":1.0}],["signals.blend.weights[1]",{"F64":1.0}],["exposure.scale",{"F64":0.5}]],"window":[1,18],"seed":0,"broker":"sim-optimal(pip_size=0.0001)"},"metrics":{"total_pips":0.26428361277333234,"max_drawdown":0.15423679012344482,"exposure_sign_flips":2}}
|
||||||
|
... (3 more members) ... exit=0
|
||||||
|
|
||||||
|
## edge: a BOGUS family id
|
||||||
|
$ aura runs family nope-99
|
||||||
|
exit=0 <-- NB: empty output + exit 0, NOT a 'no such family' diagnostic (see findings)
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
// Runway milestone fieldtest — scenario 2: the real GER40 M1 ingestion path via
|
||||||
|
// the CANONICAL 4-source OHLC opener (#80/#81/#92).
|
||||||
|
//
|
||||||
|
// The task a downstream researcher does: "open the real GER40 M1 archive and run
|
||||||
|
// my SMA-cross-on-close strategy against real bars, to completion — and I want to
|
||||||
|
// reach for nothing but `aura-ingest` to do it."
|
||||||
|
//
|
||||||
|
// What this exercises, top-down from the milestone promise:
|
||||||
|
// - #81: a real-data source builds from `aura-ingest` ALONE. This file imports
|
||||||
|
// NOTHING from `data-server`; it uses aura_ingest::{default_data_server,
|
||||||
|
// DEFAULT_DATA_PATH, open_ohlc}. The Cargo.toml has no `data-server` dep.
|
||||||
|
// - #92: `open_ohlc` is the single vetted home of the C4 merge order
|
||||||
|
// (open, high, low, close). A consumer never spells the order out.
|
||||||
|
// - #80: M1FieldSource opens a timestamp window; here via the OHLC opener over
|
||||||
|
// a closed epoch-ns [from, to].
|
||||||
|
//
|
||||||
|
// PUBLIC INTERFACE ONLY: API from `cargo doc` rustdoc + the design ledger
|
||||||
|
// (C3/C4 ingestion, C12 source seam, C7 SoA). No crates/*/src was read.
|
||||||
|
|
||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
use aura_core::{Cell, Firing, Scalar, ScalarKind, Timestamp};
|
||||||
|
use aura_engine::{
|
||||||
|
f64_field, summarize, BlueprintNode, Composite, Edge, OutField, Role, Source, Target,
|
||||||
|
};
|
||||||
|
use aura_ingest::{default_data_server, instrument_spec, open_ohlc, DEFAULT_DATA_PATH};
|
||||||
|
use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub};
|
||||||
|
|
||||||
|
const SYMBOL: &str = "GER40";
|
||||||
|
|
||||||
|
fn sma_cross() -> Composite {
|
||||||
|
Composite::new(
|
||||||
|
"sma_cross",
|
||||||
|
vec![
|
||||||
|
Sma::builder().named("fast").into(),
|
||||||
|
Sma::builder().named("slow").into(),
|
||||||
|
Sub::builder().into(),
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
||||||
|
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
||||||
|
],
|
||||||
|
vec![Role {
|
||||||
|
name: "price".into(),
|
||||||
|
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
|
||||||
|
source: None,
|
||||||
|
}],
|
||||||
|
vec![OutField { node: 2, field: 0, name: "out".into() }],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Harness with FOUR source-roles in the C4 merge order open/high/low/close, to
|
||||||
|
// mirror what open_ohlc hands back. The strategy reads CLOSE (the canonical price
|
||||||
|
// field the SMA-cross consumes). open/high/low are routed into per-field recording
|
||||||
|
// taps, so this run proves all four real OHLC streams flow to completion — not
|
||||||
|
// just close. Equity (close-driven) is recorded too.
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
fn ohlc_harness() -> (
|
||||||
|
Composite,
|
||||||
|
mpsc::Receiver<(Timestamp, Vec<Scalar>)>, // equity (close strategy)
|
||||||
|
mpsc::Receiver<(Timestamp, Vec<Scalar>)>, // open tap
|
||||||
|
mpsc::Receiver<(Timestamp, Vec<Scalar>)>, // high tap
|
||||||
|
mpsc::Receiver<(Timestamp, Vec<Scalar>)>, // low tap
|
||||||
|
) {
|
||||||
|
let (tx_eq, rx_eq) = mpsc::channel();
|
||||||
|
let (tx_o, rx_o) = mpsc::channel();
|
||||||
|
let (tx_h, rx_h) = mpsc::channel();
|
||||||
|
let (tx_l, rx_l) = mpsc::channel();
|
||||||
|
|
||||||
|
// node indices:
|
||||||
|
// 0 sma_cross (close), 1 Exposure, 2 SimBroker, 3 equity-rec,
|
||||||
|
// 4 open-rec, 5 high-rec, 6 low-rec
|
||||||
|
let bp = Composite::new(
|
||||||
|
"ohlc_harness",
|
||||||
|
vec![
|
||||||
|
BlueprintNode::Composite(sma_cross()),
|
||||||
|
Exposure::builder().into(),
|
||||||
|
SimBroker::builder(1.0).into(), // GER40 index pip = 1.0
|
||||||
|
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq).into(),
|
||||||
|
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_o).into(),
|
||||||
|
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_h).into(),
|
||||||
|
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_l).into(),
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // sma_cross -> exposure
|
||||||
|
Edge { from: 1, to: 2, slot: 0, from_field: 0 }, // exposure -> broker.slot0
|
||||||
|
Edge { from: 2, to: 3, slot: 0, from_field: 0 }, // broker equity -> rec
|
||||||
|
],
|
||||||
|
// FOUR source-roles in C4 merge order: open, high, low, close. The opener
|
||||||
|
// hands back Box<dyn Source> in exactly this order; run() maps the source
|
||||||
|
// vec positionally onto the source-roles in declaration order.
|
||||||
|
vec![
|
||||||
|
Role {
|
||||||
|
name: "open".into(),
|
||||||
|
targets: vec![Target { node: 4, slot: 0 }],
|
||||||
|
source: Some(ScalarKind::F64),
|
||||||
|
},
|
||||||
|
Role {
|
||||||
|
name: "high".into(),
|
||||||
|
targets: vec![Target { node: 5, slot: 0 }],
|
||||||
|
source: Some(ScalarKind::F64),
|
||||||
|
},
|
||||||
|
Role {
|
||||||
|
name: "low".into(),
|
||||||
|
targets: vec![Target { node: 6, slot: 0 }],
|
||||||
|
source: Some(ScalarKind::F64),
|
||||||
|
},
|
||||||
|
Role {
|
||||||
|
name: "close".into(),
|
||||||
|
// close feeds the strategy AND the broker's price slot (slot 1).
|
||||||
|
targets: vec![
|
||||||
|
Target { node: 0, slot: 0 },
|
||||||
|
Target { node: 2, slot: 1 },
|
||||||
|
],
|
||||||
|
source: Some(ScalarKind::F64),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
(bp, rx_eq, rx_o, rx_h, rx_l)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ms(t: Timestamp) -> i64 {
|
||||||
|
t.0 / 1_000_000
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// #81: the data server builds from aura-ingest alone, over the local archive.
|
||||||
|
let server = default_data_server();
|
||||||
|
println!("default_data_server() over DEFAULT_DATA_PATH = {DEFAULT_DATA_PATH}");
|
||||||
|
|
||||||
|
// The pip the milestone vouches for: a vetted index quotes in points (1.0).
|
||||||
|
match instrument_spec(SYMBOL) {
|
||||||
|
Some(spec) => println!("instrument_spec({SYMBOL}) = pip_size {}", spec.pip_size),
|
||||||
|
None => {
|
||||||
|
eprintln!("{SYMBOL} is un-specced — a real run must refuse; aborting fixture");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A ~2-month window of GER40 starting 2015-01-01 (epoch-ns; open_ohlc's unit).
|
||||||
|
let day_ns: i64 = 86_400_000 * 1_000_000;
|
||||||
|
let from = Timestamp(1_420_070_400_000 * 1_000_000); // 2015-01-01 UTC
|
||||||
|
let to = Timestamp(from.0 + 60 * day_ns);
|
||||||
|
|
||||||
|
// #92: the SINGLE vetted home of the C4 merge order. We never spell out
|
||||||
|
// open/high/low/close — the opener does, and hands back exactly four sources.
|
||||||
|
let sources: Vec<Box<dyn Source>> = match open_ohlc(&server, SYMBOL, from, to) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => {
|
||||||
|
eprintln!("no local {SYMBOL} archive overlapping the window; skipping");
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
println!(
|
||||||
|
"open_ohlc({SYMBOL}, [{}..{}]) -> {} sources (expect 4: O,H,L,C)",
|
||||||
|
ms(from),
|
||||||
|
ms(to),
|
||||||
|
sources.len(),
|
||||||
|
);
|
||||||
|
assert_eq!(sources.len(), 4, "open_ohlc vouches for four OHLC sources");
|
||||||
|
|
||||||
|
// Each source reports its data extent without materializing (C18/#71).
|
||||||
|
for (i, s) in sources.iter().enumerate() {
|
||||||
|
let field = ["open", "high", "low", "close"][i];
|
||||||
|
let b = s.bounds();
|
||||||
|
println!(
|
||||||
|
" source[{i}] {field}: bounds {:?}",
|
||||||
|
b.map(|(f, t)| (ms(f), ms(t)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (bp, rx_eq, rx_o, rx_h, rx_l) = ohlc_harness();
|
||||||
|
println!("\nharness param-space slots:");
|
||||||
|
let space = bp.param_space();
|
||||||
|
for ps in &space {
|
||||||
|
println!(" {} : {:?}", ps.name, ps.kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bootstrap with concrete params: fast=3, slow=20, exposure scale=1.0.
|
||||||
|
let params: Vec<Cell> = vec![Cell::from_i64(3), Cell::from_i64(20), Cell::from_f64(1.0)];
|
||||||
|
let mut h = bp
|
||||||
|
.bootstrap_with_cells(¶ms)
|
||||||
|
.expect("chosen params kind-check against param_space");
|
||||||
|
|
||||||
|
// Run to completion over the four REAL OHLC streams, k-way-merged by ts (C3/C4).
|
||||||
|
h.run(sources);
|
||||||
|
drop(h);
|
||||||
|
|
||||||
|
let eq_rows: Vec<_> = rx_eq.try_iter().collect();
|
||||||
|
let o_rows: Vec<_> = rx_o.try_iter().collect();
|
||||||
|
let hi_rows: Vec<_> = rx_h.try_iter().collect();
|
||||||
|
let lo_rows: Vec<_> = rx_l.try_iter().collect();
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"\nstreamed to completion: open={} high={} low={} equity={} bars recorded",
|
||||||
|
o_rows.len(),
|
||||||
|
hi_rows.len(),
|
||||||
|
lo_rows.len(),
|
||||||
|
eq_rows.len(),
|
||||||
|
);
|
||||||
|
assert!(!o_rows.is_empty(), "real open bars streamed");
|
||||||
|
assert!(!hi_rows.is_empty(), "real high bars streamed");
|
||||||
|
assert!(!lo_rows.is_empty(), "real low bars streamed");
|
||||||
|
assert!(!eq_rows.is_empty(), "close-driven equity produced");
|
||||||
|
|
||||||
|
// The four OHLC fields are the same bars: O/H/L taps share the timestamp set.
|
||||||
|
assert_eq!(o_rows.len(), hi_rows.len(), "O and H are the same bar stream");
|
||||||
|
assert_eq!(o_rows.len(), lo_rows.len(), "O and L are the same bar stream");
|
||||||
|
for k in 0..o_rows.len() {
|
||||||
|
assert_eq!(o_rows[k].0, hi_rows[k].0, "bar {k}: O/H share ts");
|
||||||
|
assert_eq!(o_rows[k].0, lo_rows[k].0, "bar {k}: O/L share ts");
|
||||||
|
}
|
||||||
|
// The OHLC ordering invariant on real bars: low <= open,high and high >= open,low.
|
||||||
|
let mut ohlc_ok = 0usize;
|
||||||
|
for k in 0..o_rows.len() {
|
||||||
|
let o = o_rows[k].1[0].as_f64();
|
||||||
|
let hi = hi_rows[k].1[0].as_f64();
|
||||||
|
let lo = lo_rows[k].1[0].as_f64();
|
||||||
|
if lo <= o && o <= hi && lo <= hi {
|
||||||
|
ohlc_ok += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"OHLC ordering (low<=open<=high) holds on {}/{} real bars",
|
||||||
|
ohlc_ok,
|
||||||
|
o_rows.len()
|
||||||
|
);
|
||||||
|
assert_eq!(ohlc_ok, o_rows.len(), "every real bar respects low<=open<=high");
|
||||||
|
|
||||||
|
let equity = f64_field(&eq_rows, 0);
|
||||||
|
// exposure stream unused here; summarize wants both — reuse equity sign? No:
|
||||||
|
// summarize needs an exposure series. We only recorded equity; report PnL only.
|
||||||
|
let final_pips = equity.last().map(|(_, v)| *v).unwrap_or(0.0);
|
||||||
|
println!(
|
||||||
|
"\nGER40 close SMA-cross over real bars: {} equity points, final cumulative = {:.2} pips (pip_size=1.0)",
|
||||||
|
equity.len(),
|
||||||
|
final_pips,
|
||||||
|
);
|
||||||
|
|
||||||
|
// first/last bar timestamps as a sanity window.
|
||||||
|
let first = o_rows.first().map(|(t, _)| ms(*t)).unwrap_or(0);
|
||||||
|
let last = o_rows.last().map(|(t, _)| ms(*t)).unwrap_or(0);
|
||||||
|
println!("real bar window actually streamed: [{first}..{last}] (unix-ms)");
|
||||||
|
|
||||||
|
// summarize is available but needs exposure; we keep the report PnL-shaped.
|
||||||
|
let _ = summarize; // referenced for clarity of the public surface
|
||||||
|
|
||||||
|
println!("\nOK: real GER40 OHLC opened from aura-ingest alone -> 4 sources, C4 order -> ran to completion.");
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
// Runway milestone fieldtest — scenario 3: multi-tap trace honesty (#93).
|
||||||
|
//
|
||||||
|
// The task a downstream researcher does: "I record two taps off the same real
|
||||||
|
// GER40 run — the exposure stream and the cumulative-equity stream — and I want a
|
||||||
|
// single per-bar trace that lines each tap up against the other. The taps fire at
|
||||||
|
// DIFFERENT cadences (exposure warms up later than the raw bar clock; equity only
|
||||||
|
// fires once the broker has a prev price), so a positional zip-by-index would
|
||||||
|
// misalign. I want them joined on the recorded TIMESTAMP."
|
||||||
|
//
|
||||||
|
// What this exercises, top-down from the milestone promise: #93's `join_on_ts` /
|
||||||
|
// `JoinedRow` — exactly one joined row per spine entry, each side Some(row) where
|
||||||
|
// it fired at that ts and None where it did not. The honest fusion the milestone
|
||||||
|
// promises: traces join on timestamp, never by index.
|
||||||
|
//
|
||||||
|
// It also re-uses the #81/#92 real-data seam (open_ohlc from aura-ingest alone) so
|
||||||
|
// the join runs over REAL bars, not a synthetic toy.
|
||||||
|
//
|
||||||
|
// PUBLIC INTERFACE ONLY: API from `cargo doc` rustdoc + the design ledger
|
||||||
|
// (C8/C18 post-run reduction, C3 no in-graph join, C1 one row per ts). No
|
||||||
|
// crates/*/src was read.
|
||||||
|
|
||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
use aura_core::{Cell, Firing, Scalar, ScalarKind, Timestamp};
|
||||||
|
use aura_engine::{join_on_ts, BlueprintNode, Composite, Edge, OutField, Role, Source, Target};
|
||||||
|
use aura_ingest::{default_data_server, open_ohlc};
|
||||||
|
use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub};
|
||||||
|
|
||||||
|
const SYMBOL: &str = "GER40";
|
||||||
|
|
||||||
|
fn sma_cross() -> Composite {
|
||||||
|
Composite::new(
|
||||||
|
"sma_cross",
|
||||||
|
vec![
|
||||||
|
Sma::builder().named("fast").into(),
|
||||||
|
Sma::builder().named("slow").into(),
|
||||||
|
Sub::builder().into(),
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
||||||
|
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
||||||
|
],
|
||||||
|
vec![Role {
|
||||||
|
name: "price".into(),
|
||||||
|
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
|
||||||
|
source: None,
|
||||||
|
}],
|
||||||
|
vec![OutField { node: 2, field: 0, name: "out".into() }],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A single-price-role harness with TWO taps off different nodes:
|
||||||
|
// - exposure tap (off the Exposure node) — fires once SMA-cross warms up
|
||||||
|
// - equity tap (off the SimBroker) — fires once the broker has a prev price
|
||||||
|
// Their cadences differ, so their recorded streams have different lengths and
|
||||||
|
// firing instants — the exact #93 shape.
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
fn harness_two_taps() -> (
|
||||||
|
Composite,
|
||||||
|
mpsc::Receiver<(Timestamp, Vec<Scalar>)>, // exposure tap
|
||||||
|
mpsc::Receiver<(Timestamp, Vec<Scalar>)>, // equity tap
|
||||||
|
) {
|
||||||
|
let (tx_ex, rx_ex) = mpsc::channel();
|
||||||
|
let (tx_eq, rx_eq) = mpsc::channel();
|
||||||
|
// 0 sma_cross, 1 Exposure, 2 SimBroker, 3 exposure-rec, 4 equity-rec
|
||||||
|
let bp = Composite::new(
|
||||||
|
"two_tap_harness",
|
||||||
|
vec![
|
||||||
|
BlueprintNode::Composite(sma_cross()),
|
||||||
|
Exposure::builder().into(),
|
||||||
|
SimBroker::builder(1.0).into(),
|
||||||
|
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex).into(),
|
||||||
|
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq).into(),
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // sma_cross -> exposure
|
||||||
|
Edge { from: 1, to: 2, slot: 0, from_field: 0 }, // exposure -> broker.slot0
|
||||||
|
Edge { from: 1, to: 3, slot: 0, from_field: 0 }, // exposure -> exposure tap
|
||||||
|
Edge { from: 2, to: 4, slot: 0, from_field: 0 }, // equity -> equity tap
|
||||||
|
],
|
||||||
|
vec![Role {
|
||||||
|
name: "price".into(),
|
||||||
|
targets: vec![
|
||||||
|
Target { node: 0, slot: 0 }, // strategy
|
||||||
|
Target { node: 2, slot: 1 }, // broker price slot
|
||||||
|
],
|
||||||
|
source: Some(ScalarKind::F64),
|
||||||
|
}],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
(bp, rx_ex, rx_eq)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ms(t: Timestamp) -> i64 {
|
||||||
|
t.0 / 1_000_000
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let server = default_data_server();
|
||||||
|
|
||||||
|
// ~1 month of GER40 from 2015-01-01 (epoch-ns), close-driven price.
|
||||||
|
let day_ns: i64 = 86_400_000 * 1_000_000;
|
||||||
|
let from = Timestamp(1_420_070_400_000 * 1_000_000);
|
||||||
|
let to = Timestamp(from.0 + 31 * day_ns);
|
||||||
|
|
||||||
|
let sources: Vec<Box<dyn Source>> = match open_ohlc(&server, SYMBOL, from, to) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => {
|
||||||
|
eprintln!("no local {SYMBOL} archive overlapping the window; skipping");
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// The strategy here uses ONE price role (close). open_ohlc gives four; we take
|
||||||
|
// the close source (index 3 in the fixed O/H/L/C order #92 vouches for).
|
||||||
|
let close: Box<dyn Source> = sources.into_iter().nth(3).expect("close is source 3");
|
||||||
|
|
||||||
|
let (bp, rx_ex, rx_eq) = harness_two_taps();
|
||||||
|
let params: Vec<Cell> = vec![Cell::from_i64(3), Cell::from_i64(20), Cell::from_f64(1.0)];
|
||||||
|
let mut h = bp.bootstrap_with_cells(¶ms).expect("params kind-check");
|
||||||
|
h.run(vec![close]);
|
||||||
|
drop(h);
|
||||||
|
|
||||||
|
let exposure_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_ex.try_iter().collect();
|
||||||
|
let equity_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_eq.try_iter().collect();
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"two taps, DIFFERENT cadences: exposure fired {} times, equity fired {} times",
|
||||||
|
exposure_rows.len(),
|
||||||
|
equity_rows.len(),
|
||||||
|
);
|
||||||
|
// The whole point of #93: a positional zip-by-index is WRONG when the lengths
|
||||||
|
// differ. Show the mismatch the index approach would hit:
|
||||||
|
if exposure_rows.len() != equity_rows.len() {
|
||||||
|
println!(
|
||||||
|
" -> lengths differ by {} rows; zip-by-index would silently misalign every row \
|
||||||
|
past the first divergence",
|
||||||
|
(exposure_rows.len() as i64 - equity_rows.len() as i64).abs(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The honest fusion: join on the recorded timestamp. Use the equity stream as
|
||||||
|
// the spine (the trace we anchor on), exposure as a side stream.
|
||||||
|
let exp_slice: &[(Timestamp, Vec<Scalar>)] = &exposure_rows;
|
||||||
|
let joined = join_on_ts(&equity_rows, &[exp_slice]);
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"\njoin_on_ts: spine(equity)={} rows -> {} JoinedRows (exactly one per spine entry)",
|
||||||
|
equity_rows.len(),
|
||||||
|
joined.len(),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
joined.len(),
|
||||||
|
equity_rows.len(),
|
||||||
|
"exactly one JoinedRow per spine entry (C1/C8)"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Every joined row's ts is the spine ts, in spine order.
|
||||||
|
for (k, jr) in joined.iter().enumerate() {
|
||||||
|
assert_eq!(jr.ts, equity_rows[k].0, "row {k}: joined ts == spine ts");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Where the exposure side fired at the spine ts -> Some; where it did not -> None.
|
||||||
|
// Cross-check Some-rows against the exposure stream by ts (not by index!).
|
||||||
|
use std::collections::HashMap;
|
||||||
|
let exp_by_ts: HashMap<i64, f64> =
|
||||||
|
exposure_rows.iter().map(|(t, r)| (t.0, r[0].as_f64())).collect();
|
||||||
|
|
||||||
|
let mut present = 0usize;
|
||||||
|
let mut absent = 0usize;
|
||||||
|
let mut mismatches = 0usize;
|
||||||
|
for jr in &joined {
|
||||||
|
match &jr.sides[0] {
|
||||||
|
Some(side_row) => {
|
||||||
|
present += 1;
|
||||||
|
let joined_val = side_row[0].as_f64();
|
||||||
|
match exp_by_ts.get(&jr.ts.0) {
|
||||||
|
Some(truth) if (*truth - joined_val).abs() < 1e-12 => {}
|
||||||
|
_ => mismatches += 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
absent += 1;
|
||||||
|
// a None side MUST mean exposure genuinely did not fire at this ts.
|
||||||
|
if exp_by_ts.contains_key(&jr.ts.0) {
|
||||||
|
mismatches += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"exposure side: present(Some)={present}, absent(None)={absent}, by-ts cross-check mismatches={mismatches}",
|
||||||
|
);
|
||||||
|
assert_eq!(mismatches, 0, "every Some/None matches the exposure stream BY TIMESTAMP");
|
||||||
|
|
||||||
|
// Show the first few divergent rows: spine bars where exposure had NOT yet
|
||||||
|
// warmed up (None) vs where both fired (Some) — proving the alignment is by ts.
|
||||||
|
println!("\nfirst 6 joined rows (equity spine | exposure side):");
|
||||||
|
for jr in joined.iter().take(6) {
|
||||||
|
let eq = jr.spine[0].as_f64();
|
||||||
|
let ex = jr.sides[0].as_ref().map(|r| r[0].as_f64());
|
||||||
|
println!(
|
||||||
|
" ts={} equity={:.4} exposure={}",
|
||||||
|
ms(jr.ts),
|
||||||
|
eq,
|
||||||
|
match ex {
|
||||||
|
Some(v) => format!("Some({v:.4})"),
|
||||||
|
None => "None (had not fired at this ts)".into(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"\nOK: two real-data taps at different cadences fused HONESTLY on timestamp \
|
||||||
|
({present} aligned, {absent} side-absent), never by index."
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user