diff --git a/docs/specs/fieldtest-sweep-dissolution.md b/docs/specs/fieldtest-sweep-dissolution.md new file mode 100644 index 0000000..9164022 --- /dev/null +++ b/docs/specs/fieldtest-sweep-dissolution.md @@ -0,0 +1,245 @@ +# Fieldtest — cycle 0110 (sweep dissolution) — 2026-07-04 + +**Status:** Draft — awaiting orchestrator triage +**Author:** fieldtester (dispatched by fieldtest skill) +**Binary exercised:** `target/debug/aura` (workspace built from HEAD +`2c72996`; project cdylib `liblab.so` built from the scaffolded `lab/` crate). + +## Scope +Cycle 0110 is cycle 1 of the "verb dissolution" milestone (#210). Two +user-visible changes: (1) the `std::sweep` process block's selection triple +(`metric`+`select`+`deflate`) became an optional, all-or-nothing selection +group — omit it for a **selection-free sweep**, permitted only as the +pipeline's terminal stage; (2) `aura sweep --real SYM --from +--to --axis k=v,…` is now thin sugar over the campaign engine: it +auto-registers a generated selection-free process document + a campaign +document (content-addressed) and appends a campaign-run record, printing the +same member lines as before (member manifests now carry an `instrument` +stamp). This fieldtest exercises both, plus the two consumer paths the +selection-free sweep exists for (refusal edges, rank-later). + +## Examples +All fixtures under `fieldtests/cycle-0110-sweep-dissolution/`; the scaffolded +project is `lab/` (its `runs/` store is kept as evidence, `target/` removed). +Real data: GER40, Sep 2024 (`--from 1725148800000 --to 1727740799999`). + +### c0110_1_explore_sweep.json — selection-free single-stage process document +- A bare `{"block":"std::sweep"}` pipeline authored straight from + authoring-guide §2. +- Fits scope: exercises the new optional-selection-group surface at its + simplest (the omitted group). +- Outcome: `process validate` → `valid (intrinsic): 1 pipeline blocks`; + `process register` → id `78f2d8d7…`, re-register dedupes (one file); + stored canonical form is byte-exactly `…"pipeline":[{"block":"std::sweep"}]`. + `process introspect --block std::sweep` matches the guide verbatim + (metric/select/deflate all "optional"). **Ran, matched expected.** + +### c0110_2_smacross_open_op.json (+ built blueprint) — the dissolved verb +- Unbound SMA-crossover op-script → `graph build` → blueprint with three open + axes; `aura sweep --real GER40 --from … --to … --axis + graph.fast.length=2,4 --axis graph.slow.length=8,16 --axis + graph.bias.scale=0.5 --name c0110-explore`. +- Fits scope: this *is* the dissolved verb; a realistic ad-hoc explore-sweep. +- Outcome: prints 4 GER40-stamped member lines; auto-registers a generated + process (`9227c300`, name "sweep", selection-free) + campaign (`46e4f7ef`, + raw axis names, GER40, ms sub-range window) and appends one + `campaign_runs.jsonl` record; identical re-run **dedupes both documents** + (no new files, same ids) while appending a second realization (family + suffix `s0-0`→`s0-1`, metrics bit-identical — C1); `campaign runs` lists + both. **Ran, matched expected.** (`c0110_2_smacross_op.json`, the *bound* + variant, is kept as evidence for finding F3.) + +### c0110_3a_metric_only.json / c0110_3a_select_only.json / c0110_3b_* — refusal edges +- Half-populated selection group (metric-only, select-only); a selection-free + sweep followed by a `std::gate` stage, driven to the executor via + `c0110_3b_campaign_nonterminal.json`. +- Fits scope: the two new refusals (all-or-nothing group; terminal-only). +- Outcome: half-group → clean intrinsic refusal naming the missing slot; + non-terminal free sweep → passes intrinsic, refused at the executable tier + and at `campaign run` with clear prose, no realization appended. **Ran, + matched expected.** + +### (Task 4) rank-later over the persisted family — `aura runs family … rank …` +- No new fixture; consumes the `46e4f7ef-0-GER40-w0-s0-0` family persisted by + example 2. +- Fits scope: the rank-later pattern the selection-free sweep exists for. +- Outcome: `rank sqn` / `rank expectancy_r` return the 4 members best-first; + ranking respects metric directionality (max_drawdown ascending, sqn + descending); the rank roster equals the `rankable` metric roster + (`win_rate`/nonexistent refused with the known list). **Ran, matched + expected.** + +## Findings + +### [working] Selection-free process document round-trips cleanly +- Example 1. +- `process validate` → `process document valid (intrinsic): 1 pipeline + blocks, 0 gate predicates`; register id `78f2d8d7…`, idempotent on + re-register; stored form is the bare block; `introspect --block std::sweep` + output is byte-for-byte the guide's transcription. +- Working: the new optional group is reachable, authorable from the doc alone, + and the introspection self-description matches the ledger/guide. +- Action: carry-on. + +### [working] The dissolved sweep verb generates + dedupes a selection-free campaign +- Example 2. +- One invocation produced the generated process `9227c300` + (`{"name":"sweep","pipeline":[{"block":"std::sweep"}]}`), campaign + `46e4f7ef` (raw axes `fast.length`/`slow.length`/`bias.scale`, `instrument: + GER40`, window `[1725236099999,1727726220000]` — a ms sub-range of the + request), and one `campaign_runs.jsonl` line. Identical re-run: **no new + documents** (same content ids), a second realization (`run 1`, family suffix + `s0-1`), member metrics bit-identical. `campaign runs` lists both runs. +- Working: the sugar→campaign parity, content-addressed dedup, deterministic + accumulation (C1), and the additive `instrument` stamp all hold end to end. +- Action: carry-on. + +### [working] All-or-nothing + terminal-only refusals are clear and guide-matching +- Examples 3a/3b. +- metric-only → `block std::sweep: slot "metric" without "select" — the + selection group is all-or-nothing`; select-only symmetric; non-terminal free + sweep → `campaign is not executable: process stage 0: a sweep without a + selection group (metric + select) must be the last stage of its process` + (same prose at `campaign run`, no realization appended). +- Working: both new refusals fire at the right tier with comprehensible prose + that matches authoring-guide §2. +- Action: carry-on. + +### [working] Rank-later over the persisted family, best-first, roster-enforced +- Task 4. +- `rank sqn` orders 1.054 > 0.849 > 0.052 > −0.545; `rank max_drawdown` orders + 4.22M < 6.16M < 6.57M < 9.14M (ascending — lower drawdown ranked better); + `rank win_rate` → `unknown metric 'win_rate' (known: total_pips, + max_drawdown, bias_sign_flips, sqn, sqn_normalized, expectancy_r, + net_expectancy_r)`. +- Working: the selection-free sweep's reason to exist (defer selection to a + rank-later step) works, and the rank roster equals the documented `rankable` + set. (Direction convention noted as F-min below.) +- Action: carry-on. + +### [spec_gap] `aura sweep --axis` requires the `graph.`-wrapped name; the docs teach the raw form +- Example 2 (works with `graph.fast.length`) vs probe P2 (fails with + `fast.length`). +- `graph introspect --params` prints `fast.length` and the authoring guide + (§1) calls that "the raw param-space namespace (what a campaign's axes bind + against)". The stored campaign doc also uses the raw form. But the dissolved + `aura sweep --axis` accepts **only** the wrapped `graph.` form — + discoverable solely via `aura sweep --list-axes`, which the authoring + guide never mentions (the guide does not document `aura sweep` at all; the + glossary and `--help` say only `--axis name=`). +- Why spec_gap: the public interface documents the raw namespace as canonical + and is silent on the wrapper; a consumer following it picks the wrong form. + The wrapper leaks the root-composite instance name (`graph`) into the user's + axis vocabulary. +- Recommended action: document (glossary `sweep` + a note in authoring-guide) + that `aura sweep --axis` uses the `--list-axes` (wrapped) names, **or** make + the dispatch accept the raw `--params` names too. → tighten the design + ledger / docs (+ plan if the surface is to accept raw). + +### [friction] The wrapped-name refusal mangles the axis the user typed +- Probe P2: `aura sweep … --axis fast.length=2,4 --axis slow.length=8,16 + --axis bias.scale=0.5` → + `strategy 597d…: axis "length" is not in the param space` / + `axis "scale" is not in the param space` / + `open param "fast.length" is bound by no campaign axis` … +- The `#203` first-segment strip consumes the leading `fast`/`slow`/`bias` + segment, so the diagnostic names `length`/`scale` — fragments the user never + typed as a whole axis — while separately reporting the real params as + "bound by no axis". The two same-stripped legs (`fast.length`,`slow.length`) + even collapse to a single `length` key. The message never says "a leading + segment was stripped as the graph name". +- Why friction: the task can be completed (use the wrapped name), but the + refusal actively misdirects — it names a mangled fragment instead of echoing + the input and explaining the wrapper convention. (Same root cause as the + spec_gap above; split per the one-class-per-finding rule.) +- Recommended action: echo the typed axis and/or name the expected wrapper in + the refusal. → plan (tidy iteration). + +### [spec_gap] Authoring-guide §1 inline SMA-crossover binds `length`, contradicting every id / `--params` / §3 axis it prints +- Encountered building the sweep blueprint (Task 2); evidence + `c0110_2_smacross_op.json`. +- The guide's §1 worked op-script binds `fast.length=2` and `slow.length=4` + and claims (line 72) it matches the on-disk corpus + `mra_1_strategy_smacross.json`. It does not: the on-disk file leaves both + **unbound**. The bound inline form hashes to `e2be81b2…` with `--params` = + `bias.scale` only; the unbound corpus form hashes to `597d719b…` (the id the + guide later prints at §1/§3 and the id every campaign example references) + with `--params` = `fast.length`,`slow.length`,`bias.scale`. A reader copying + the inline example gets a blueprint whose id matches nothing in the guide and + whose `fast.length`/`slow.length` are closed — so authoring-guide §3's + campaign (axes `fast.length`,`slow.length`) would refuse against it. +- Why spec_gap: the guide is internally inconsistent, and the underlying rule — + that an op-script `bind` *pins a param out of the sweepable space* (not a + sweep-overridable default) — is nowhere stated. Note the 0110 audit commit + claimed "worked example verified against the live binary"; this §1 example + (authored earlier, #197/#208) was not. +- Recommended action: drop the `bind` from the §1 inline example (or state + that binding closes the axis) and document `bind`-vs-axis interaction. → + tighten docs. + +### [friction] `aura sweep ` rejects the op-script array with a raw serde error +- Probe: `aura sweep c0110_2_smacross_open_op.json --list-axes` → + `aura: …_op.json: Json(Error("invalid type: map, expected u32", line: 2, + column: 2))`. +- `graph introspect --content-id/--params/register` auto-discriminate the + op-script array vs the built envelope (guide §1), but `aura sweep`'s + blueprint slot accepts only the built envelope, and the mismatch surfaces as + a leaked serde `Json(Error(...))` rather than a house-style + "expected a built blueprint; run `aura graph build` first". The guide never + says `aura sweep` needs a pre-built blueprint. +- Why friction: the path completes once you build first, but the diagnostic is + an internal-type leak (contrast #162's Display-free-error convention seen + elsewhere) and the required build step is undocumented for `aura sweep`. +- Recommended action: shape-discriminate (as `graph introspect` does) or emit + a house-style hint. → plan. + +### [friction] Refused dissolved sweeps still register their generated campaign document (store litter, some malformed) +- Probes P2 (raw names) and P3 (subset axes) both **refused** at the + referential tier, yet each left a generated campaign doc in + `runs/campaigns/` (`92c66b18…` "c0110-rawname", `b0e3f90b…` "c0110-subset"). + `campaign_runs.jsonl` correctly stayed at 2 (no realization). The rawname + doc is *malformed*: its axes are the stripped/collapsed `{"length":[8,16], + "scale":[0.5]}` — a document the tool itself immediately rejects, now + permanent in the content-addressed store. +- Why friction: the sugar registers the generated process+campaign documents + *before* referential/executable validation, so a fat-fingered axis pollutes + the store with dead (occasionally malformed) documents. Content-addressing + bounds the growth (identical bad invocations dedupe), and no realization is + recorded, so nothing downstream breaks — hence friction, not bug. + (`campaign run ` registering before an executable refusal — the Task-3 + `d10ed054…` doc — is by contrast *documented* register-then-run behaviour and + is not flagged.) +- Recommended action: in the sweep sugar, validate the generated documents + (referential/executable) before `register_generated`, registering only on a + clean preflight. → plan. + +### [friction] `process`/`campaign` intrinsic validate needs the built project cdylib when inside a project +- Task 1 setup: inside the fresh `lab/` project, `process validate + c0110_1_explore_sweep.json` (and register/introspect) first failed with + `aura: cargo metadata failed: no cdylib target …`, then after `cargo build` + with `aura: project dylib not found … run cargo build`. The very same + `process validate` runs **dylib-free outside any project** (probe P1 → + `valid (intrinsic)`). +- Why friction: a std-only process document references no project nodes, so + the intrinsic (and referential-over-std) check needs no compiled dylib; the + CLI nonetheless loads the project env eagerly for any `process`/`campaign` + subcommand once an `Aura.toml` is found. A newcomer following the guide + inside a fresh project hits a build wall on a pure-shape check. (Likely + pre-existing, not introduced by 0110; the error is actionable.) +- Recommended action: let the intrinsic tier run before/without the dylib load + inside a project, or document that any in-project `process`/`campaign` call + requires a built cdylib. → plan or ratify (decide the intended coupling). + +## Recommendation summary +| Finding | Class | Action | +|---|---|---| +| Selection-free process doc round-trips | working | carry-on | +| Dissolved sweep → deduped selection-free campaign | working | carry-on | +| All-or-nothing + terminal-only refusals clear | working | carry-on | +| Rank-later best-first, roster-enforced | working | carry-on | +| `aura sweep --axis` needs wrapped name, docs teach raw | spec_gap | tighten docs (+ plan if raw to be accepted) | +| Wrapped-name refusal mangles the typed axis | friction | plan | +| Guide §1 inline binds `length`, inconsistent with its ids/axes | spec_gap | tighten docs | +| `aura sweep ` leaks a serde error | friction | plan | +| Refused sweeps still register their generated campaign doc | friction | plan | +| In-project intrinsic validate needs the built cdylib | friction | plan / ratify | diff --git a/fieldtests/cycle-0110-sweep-dissolution/c0110_1_explore_sweep.json b/fieldtests/cycle-0110-sweep-dissolution/c0110_1_explore_sweep.json new file mode 100644 index 0000000..39ab5ab --- /dev/null +++ b/fieldtests/cycle-0110-sweep-dissolution/c0110_1_explore_sweep.json @@ -0,0 +1,6 @@ +{ + "format_version": 1, + "kind": "process", + "name": "explore-only-sweep", + "pipeline": [ { "block": "std::sweep" } ] +} diff --git a/fieldtests/cycle-0110-sweep-dissolution/c0110_2_smacross_op.json b/fieldtests/cycle-0110-sweep-dissolution/c0110_2_smacross_op.json new file mode 100644 index 0000000..53e7cac --- /dev/null +++ b/fieldtests/cycle-0110-sweep-dissolution/c0110_2_smacross_op.json @@ -0,0 +1,12 @@ +[ + {"op": "source", "role": "price", "kind": "F64"}, + {"op": "add", "type": "SMA", "name": "fast", "bind": {"length": {"I64": 2}}}, + {"op": "add", "type": "SMA", "name": "slow", "bind": {"length": {"I64": 4}}}, + {"op": "feed", "role": "price", "into": ["fast.series", "slow.series"]}, + {"op": "add", "type": "Sub", "name": "sub"}, + {"op": "connect", "from": "fast.value", "to": "sub.lhs"}, + {"op": "connect", "from": "slow.value", "to": "sub.rhs"}, + {"op": "add", "type": "Bias", "name": "bias"}, + {"op": "connect", "from": "sub.value", "to": "bias.signal"}, + {"op": "expose", "from": "bias.bias", "as": "bias"} +] diff --git a/fieldtests/cycle-0110-sweep-dissolution/c0110_2_smacross_open_blueprint.json b/fieldtests/cycle-0110-sweep-dissolution/c0110_2_smacross_open_blueprint.json new file mode 100644 index 0000000..83ba539 --- /dev/null +++ b/fieldtests/cycle-0110-sweep-dissolution/c0110_2_smacross_open_blueprint.json @@ -0,0 +1 @@ +{"format_version":1,"blueprint":{"name":"graph","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow"}},{"primitive":{"type":"Sub","name":"sub"}},{"primitive":{"type":"Bias","name":"bias"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}} \ No newline at end of file diff --git a/fieldtests/cycle-0110-sweep-dissolution/c0110_2_smacross_open_op.json b/fieldtests/cycle-0110-sweep-dissolution/c0110_2_smacross_open_op.json new file mode 100644 index 0000000..ccbeb1a --- /dev/null +++ b/fieldtests/cycle-0110-sweep-dissolution/c0110_2_smacross_open_op.json @@ -0,0 +1,12 @@ +[ + {"op": "source", "role": "price", "kind": "F64"}, + {"op": "add", "type": "SMA", "name": "fast"}, + {"op": "add", "type": "SMA", "name": "slow"}, + {"op": "feed", "role": "price", "into": ["fast.series", "slow.series"]}, + {"op": "add", "type": "Sub", "name": "sub"}, + {"op": "connect", "from": "fast.value", "to": "sub.lhs"}, + {"op": "connect", "from": "slow.value", "to": "sub.rhs"}, + {"op": "add", "type": "Bias", "name": "bias"}, + {"op": "connect", "from": "sub.value", "to": "bias.signal"}, + {"op": "expose", "from": "bias.bias", "as": "bias"} +] diff --git a/fieldtests/cycle-0110-sweep-dissolution/c0110_2_sweep_run1_stdout.txt b/fieldtests/cycle-0110-sweep-dissolution/c0110_2_sweep_run1_stdout.txt new file mode 100644 index 0000000..5121b33 --- /dev/null +++ b/fieldtests/cycle-0110-sweep-dissolution/c0110_2_sweep_run1_stdout.txt @@ -0,0 +1,4 @@ +{"family_id":"46e4f7ef-0-GER40-w0-s0-0","report":{"manifest":{"commit":"2c729965db2b998376b844a6ae4031e9f747ec7c","params":[["graph.fast.length",{"I64":2}],["graph.slow.length",{"I64":8}],["graph.bias.scale",{"F64":0.5}]],"window":[1725236099999000000,1727726220000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","instrument":"GER40","topology_hash":"597d719b7ac607158cda3e68cd497387620397a5e93087e23da512876dafba9a","project":{"namespace":"lab","dylib_sha256":"4b15c65f11584b3999ace595568f0b1377a5e0a45fd7e081701e64f64cea8679","commit":"2c729965db2b998376b844a6ae4031e9f747ec7c-dirty"}},"metrics":{"total_pips":13117250.0000001,"max_drawdown":6156000.00000006,"bias_sign_flips":3977,"r":{"expectancy_r":0.028321444460569407,"n_trades":3903,"win_rate":0.33692031770433,"avg_win_r":1.5089377461851683,"avg_loss_r":-0.7240009808747663,"profit_factor":1.0589943641136834,"max_r_drawdown":78.64539272099334,"n_open_at_end":1,"sqn":1.0540214890316064,"sqn_normalized":0.16871363772287948,"net_expectancy_r":0.028321444460569407,"conviction_terciles_r":[-0.05124882989934285,0.06705254021274346,0.0691606230683076]}}}} +{"family_id":"46e4f7ef-0-GER40-w0-s0-0","report":{"manifest":{"commit":"2c729965db2b998376b844a6ae4031e9f747ec7c","params":[["graph.fast.length",{"I64":2}],["graph.slow.length",{"I64":16}],["graph.bias.scale",{"F64":0.5}]],"window":[1725236099999000000,1727726220000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","instrument":"GER40","topology_hash":"597d719b7ac607158cda3e68cd497387620397a5e93087e23da512876dafba9a","project":{"namespace":"lab","dylib_sha256":"4b15c65f11584b3999ace595568f0b1377a5e0a45fd7e081701e64f64cea8679","commit":"2c729965db2b998376b844a6ae4031e9f747ec7c-dirty"}},"metrics":{"total_pips":358625.0000013883,"max_drawdown":6570250.000000078,"bias_sign_flips":2550,"r":{"expectancy_r":0.001942726488289421,"n_trades":2581,"win_rate":0.2909724912824487,"avg_win_r":1.9096186007126963,"avg_loss_r":-0.7809340940267545,"profit_factor":1.0035086026962674,"max_r_drawdown":72.50403168697483,"n_open_at_end":1,"sqn":0.05152717930726507,"sqn_normalized":0.010142438121122548,"net_expectancy_r":0.001942726488289421,"conviction_terciles_r":[0.003799692660105443,-0.03634478563974999,0.0383309605444476]}}}} +{"family_id":"46e4f7ef-0-GER40-w0-s0-0","report":{"manifest":{"commit":"2c729965db2b998376b844a6ae4031e9f747ec7c","params":[["graph.fast.length",{"I64":4}],["graph.slow.length",{"I64":8}],["graph.bias.scale",{"F64":0.5}]],"window":[1725236099999000000,1727726220000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","instrument":"GER40","topology_hash":"597d719b7ac607158cda3e68cd497387620397a5e93087e23da512876dafba9a","project":{"namespace":"lab","dylib_sha256":"4b15c65f11584b3999ace595568f0b1377a5e0a45fd7e081701e64f64cea8679","commit":"2c729965db2b998376b844a6ae4031e9f747ec7c-dirty"}},"metrics":{"total_pips":7427500.00000102,"max_drawdown":4223625.000000085,"bias_sign_flips":3570,"r":{"expectancy_r":0.025765945651374078,"n_trades":3811,"win_rate":0.358960902650223,"avg_win_r":1.508960221588393,"avg_loss_r":-0.8047742792695608,"profit_factor":1.0499444782267588,"max_r_drawdown":90.38863380823058,"n_open_at_end":1,"sqn":0.8490217221109733,"sqn_normalized":0.1375305967356901,"net_expectancy_r":0.025765945651374078,"conviction_terciles_r":[0.016131381474867954,-0.0014331659738562395,0.06257064137773533]}}}} +{"family_id":"46e4f7ef-0-GER40-w0-s0-0","report":{"manifest":{"commit":"2c729965db2b998376b844a6ae4031e9f747ec7c","params":[["graph.fast.length",{"I64":4}],["graph.slow.length",{"I64":16}],["graph.bias.scale",{"F64":0.5}]],"window":[1725236099999000000,1727726220000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","instrument":"GER40","topology_hash":"597d719b7ac607158cda3e68cd497387620397a5e93087e23da512876dafba9a","project":{"namespace":"lab","dylib_sha256":"4b15c65f11584b3999ace595568f0b1377a5e0a45fd7e081701e64f64cea8679","commit":"2c729965db2b998376b844a6ae4031e9f747ec7c-dirty"}},"metrics":{"total_pips":-700187.4999997262,"max_drawdown":9144874.999999981,"bias_sign_flips":1965,"r":{"expectancy_r":-0.021911292659493667,"n_trades":2388,"win_rate":0.3274706867671692,"avg_win_r":1.76727288256566,"avg_loss_r":-0.8931080703843195,"profit_factor":0.9635201732345389,"max_r_drawdown":132.7156533736896,"n_open_at_end":1,"sqn":-0.5448524993019401,"sqn_normalized":-0.11149664160230002,"net_expectancy_r":-0.021911292659493667,"conviction_terciles_r":[-0.0043670430492390765,-0.0543615931213561,-0.007005241807885511]}}}} diff --git a/fieldtests/cycle-0110-sweep-dissolution/c0110_2_sweep_run2_stdout.txt b/fieldtests/cycle-0110-sweep-dissolution/c0110_2_sweep_run2_stdout.txt new file mode 100644 index 0000000..9c1d25c --- /dev/null +++ b/fieldtests/cycle-0110-sweep-dissolution/c0110_2_sweep_run2_stdout.txt @@ -0,0 +1,4 @@ +{"family_id":"46e4f7ef-0-GER40-w0-s0-1","report":{"manifest":{"commit":"2c729965db2b998376b844a6ae4031e9f747ec7c","params":[["graph.fast.length",{"I64":2}],["graph.slow.length",{"I64":8}],["graph.bias.scale",{"F64":0.5}]],"window":[1725236099999000000,1727726220000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","instrument":"GER40","topology_hash":"597d719b7ac607158cda3e68cd497387620397a5e93087e23da512876dafba9a","project":{"namespace":"lab","dylib_sha256":"4b15c65f11584b3999ace595568f0b1377a5e0a45fd7e081701e64f64cea8679","commit":"2c729965db2b998376b844a6ae4031e9f747ec7c-dirty"}},"metrics":{"total_pips":13117250.0000001,"max_drawdown":6156000.00000006,"bias_sign_flips":3977,"r":{"expectancy_r":0.028321444460569407,"n_trades":3903,"win_rate":0.33692031770433,"avg_win_r":1.5089377461851683,"avg_loss_r":-0.7240009808747663,"profit_factor":1.0589943641136834,"max_r_drawdown":78.64539272099334,"n_open_at_end":1,"sqn":1.0540214890316064,"sqn_normalized":0.16871363772287948,"net_expectancy_r":0.028321444460569407,"conviction_terciles_r":[-0.05124882989934285,0.06705254021274346,0.0691606230683076]}}}} +{"family_id":"46e4f7ef-0-GER40-w0-s0-1","report":{"manifest":{"commit":"2c729965db2b998376b844a6ae4031e9f747ec7c","params":[["graph.fast.length",{"I64":2}],["graph.slow.length",{"I64":16}],["graph.bias.scale",{"F64":0.5}]],"window":[1725236099999000000,1727726220000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","instrument":"GER40","topology_hash":"597d719b7ac607158cda3e68cd497387620397a5e93087e23da512876dafba9a","project":{"namespace":"lab","dylib_sha256":"4b15c65f11584b3999ace595568f0b1377a5e0a45fd7e081701e64f64cea8679","commit":"2c729965db2b998376b844a6ae4031e9f747ec7c-dirty"}},"metrics":{"total_pips":358625.0000013883,"max_drawdown":6570250.000000078,"bias_sign_flips":2550,"r":{"expectancy_r":0.001942726488289421,"n_trades":2581,"win_rate":0.2909724912824487,"avg_win_r":1.9096186007126963,"avg_loss_r":-0.7809340940267545,"profit_factor":1.0035086026962674,"max_r_drawdown":72.50403168697483,"n_open_at_end":1,"sqn":0.05152717930726507,"sqn_normalized":0.010142438121122548,"net_expectancy_r":0.001942726488289421,"conviction_terciles_r":[0.003799692660105443,-0.03634478563974999,0.0383309605444476]}}}} +{"family_id":"46e4f7ef-0-GER40-w0-s0-1","report":{"manifest":{"commit":"2c729965db2b998376b844a6ae4031e9f747ec7c","params":[["graph.fast.length",{"I64":4}],["graph.slow.length",{"I64":8}],["graph.bias.scale",{"F64":0.5}]],"window":[1725236099999000000,1727726220000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","instrument":"GER40","topology_hash":"597d719b7ac607158cda3e68cd497387620397a5e93087e23da512876dafba9a","project":{"namespace":"lab","dylib_sha256":"4b15c65f11584b3999ace595568f0b1377a5e0a45fd7e081701e64f64cea8679","commit":"2c729965db2b998376b844a6ae4031e9f747ec7c-dirty"}},"metrics":{"total_pips":7427500.00000102,"max_drawdown":4223625.000000085,"bias_sign_flips":3570,"r":{"expectancy_r":0.025765945651374078,"n_trades":3811,"win_rate":0.358960902650223,"avg_win_r":1.508960221588393,"avg_loss_r":-0.8047742792695608,"profit_factor":1.0499444782267588,"max_r_drawdown":90.38863380823058,"n_open_at_end":1,"sqn":0.8490217221109733,"sqn_normalized":0.1375305967356901,"net_expectancy_r":0.025765945651374078,"conviction_terciles_r":[0.016131381474867954,-0.0014331659738562395,0.06257064137773533]}}}} +{"family_id":"46e4f7ef-0-GER40-w0-s0-1","report":{"manifest":{"commit":"2c729965db2b998376b844a6ae4031e9f747ec7c","params":[["graph.fast.length",{"I64":4}],["graph.slow.length",{"I64":16}],["graph.bias.scale",{"F64":0.5}]],"window":[1725236099999000000,1727726220000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","instrument":"GER40","topology_hash":"597d719b7ac607158cda3e68cd497387620397a5e93087e23da512876dafba9a","project":{"namespace":"lab","dylib_sha256":"4b15c65f11584b3999ace595568f0b1377a5e0a45fd7e081701e64f64cea8679","commit":"2c729965db2b998376b844a6ae4031e9f747ec7c-dirty"}},"metrics":{"total_pips":-700187.4999997262,"max_drawdown":9144874.999999981,"bias_sign_flips":1965,"r":{"expectancy_r":-0.021911292659493667,"n_trades":2388,"win_rate":0.3274706867671692,"avg_win_r":1.76727288256566,"avg_loss_r":-0.8931080703843195,"profit_factor":0.9635201732345389,"max_r_drawdown":132.7156533736896,"n_open_at_end":1,"sqn":-0.5448524993019401,"sqn_normalized":-0.11149664160230002,"net_expectancy_r":-0.021911292659493667,"conviction_terciles_r":[-0.0043670430492390765,-0.0543615931213561,-0.007005241807885511]}}}} diff --git a/fieldtests/cycle-0110-sweep-dissolution/c0110_3a_metric_only.json b/fieldtests/cycle-0110-sweep-dissolution/c0110_3a_metric_only.json new file mode 100644 index 0000000..0ccb0bb --- /dev/null +++ b/fieldtests/cycle-0110-sweep-dissolution/c0110_3a_metric_only.json @@ -0,0 +1,6 @@ +{ + "format_version": 1, + "kind": "process", + "name": "half-group-metric-only", + "pipeline": [ { "block": "std::sweep", "metric": "sqn" } ] +} diff --git a/fieldtests/cycle-0110-sweep-dissolution/c0110_3a_select_only.json b/fieldtests/cycle-0110-sweep-dissolution/c0110_3a_select_only.json new file mode 100644 index 0000000..f2033a6 --- /dev/null +++ b/fieldtests/cycle-0110-sweep-dissolution/c0110_3a_select_only.json @@ -0,0 +1,6 @@ +{ + "format_version": 1, + "kind": "process", + "name": "half-group-select-only", + "pipeline": [ { "block": "std::sweep", "select": "argmax" } ] +} diff --git a/fieldtests/cycle-0110-sweep-dissolution/c0110_3b_campaign_nonterminal.json b/fieldtests/cycle-0110-sweep-dissolution/c0110_3b_campaign_nonterminal.json new file mode 100644 index 0000000..114b3cd --- /dev/null +++ b/fieldtests/cycle-0110-sweep-dissolution/c0110_3b_campaign_nonterminal.json @@ -0,0 +1,22 @@ +{ + "format_version": 1, + "kind": "campaign", + "name": "c0110-refusal-nonterminal", + "seed": 0, + "data": { + "instruments": ["GER40"], + "windows": [ { "from_ms": 1725148800000, "to_ms": 1727740799999 } ] + }, + "strategies": [ + { + "ref": { "content_id": "597d719b7ac607158cda3e68cd497387620397a5e93087e23da512876dafba9a" }, + "axes": { + "fast.length": { "kind": "I64", "values": [2, 4] }, + "slow.length": { "kind": "I64", "values": [8, 16] }, + "bias.scale": { "kind": "F64", "values": [0.5] } + } + } + ], + "process": { "ref": { "content_id": "176cc25a03e2fc6c342eb1ec0ac50ad12e4d6e0375a21086918e64afdbd277ec" } }, + "presentation": { "persist_taps": [], "emit": ["family_table"] } +} diff --git a/fieldtests/cycle-0110-sweep-dissolution/c0110_3b_free_sweep_then_gate.json b/fieldtests/cycle-0110-sweep-dissolution/c0110_3b_free_sweep_then_gate.json new file mode 100644 index 0000000..fb152e7 --- /dev/null +++ b/fieldtests/cycle-0110-sweep-dissolution/c0110_3b_free_sweep_then_gate.json @@ -0,0 +1,9 @@ +{ + "format_version": 1, + "kind": "process", + "name": "free-sweep-then-gate", + "pipeline": [ + { "block": "std::sweep" }, + { "block": "std::gate", "all": [ { "metric": "expectancy_r", "cmp": "gt", "value": 0.0 } ] } + ] +} diff --git a/fieldtests/cycle-0110-sweep-dissolution/lab/.gitignore b/fieldtests/cycle-0110-sweep-dissolution/lab/.gitignore new file mode 100644 index 0000000..af5cf3d --- /dev/null +++ b/fieldtests/cycle-0110-sweep-dissolution/lab/.gitignore @@ -0,0 +1,3 @@ +/target +Cargo.lock +/runs diff --git a/fieldtests/cycle-0110-sweep-dissolution/lab/Aura.toml b/fieldtests/cycle-0110-sweep-dissolution/lab/Aura.toml new file mode 100644 index 0000000..d45dade --- /dev/null +++ b/fieldtests/cycle-0110-sweep-dissolution/lab/Aura.toml @@ -0,0 +1,4 @@ +# Static project context only (C17); paths only. +[paths] +runs = "runs" +# data = "/path/to/archive" # the recorded-data root; defaults to the built-in path diff --git a/fieldtests/cycle-0110-sweep-dissolution/lab/CLAUDE.md b/fieldtests/cycle-0110-sweep-dissolution/lab/CLAUDE.md new file mode 100644 index 0000000..28e02f7 --- /dev/null +++ b/fieldtests/cycle-0110-sweep-dissolution/lab/CLAUDE.md @@ -0,0 +1,10 @@ +# lab — an aura research project + +This crate is an aura project: a cdylib of node/strategy blueprints the +`aura` host loads during research (see the engine's docs/project-layout.md). + +- Build: `cargo build` (the next `aura` invocation loads the fresh dylib) +- Run: `aura run blueprints/signal.json` (from anywhere inside this dir) +- Nodes live in `src/`; each is registered in `vocabulary()`/`type_ids()` + under the `lab::` namespace prefix. +- Topology is data (`blueprints/*.json`), node logic is Rust. diff --git a/fieldtests/cycle-0110-sweep-dissolution/lab/Cargo.toml b/fieldtests/cycle-0110-sweep-dissolution/lab/Cargo.toml new file mode 100644 index 0000000..6915892 --- /dev/null +++ b/fieldtests/cycle-0110-sweep-dissolution/lab/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "lab" +version = "0.1.0" +edition = "2024" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +aura-core = { path = "/home/brummel/dev/aura/crates/aura-core" } + +# Standalone workspace root: never a member of an enclosing workspace. +[workspace] diff --git a/fieldtests/cycle-0110-sweep-dissolution/lab/blueprints/signal.json b/fieldtests/cycle-0110-sweep-dissolution/lab/blueprints/signal.json new file mode 100644 index 0000000..200ccbc --- /dev/null +++ b/fieldtests/cycle-0110-sweep-dissolution/lab/blueprints/signal.json @@ -0,0 +1 @@ +{"format_version":1,"blueprint":{"name":"lab_signal","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}},{"primitive":{"type":"lab::Identity"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0},{"from":3,"to":4,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":4,"field":0,"name":"bias"}]}} \ No newline at end of file diff --git a/fieldtests/cycle-0110-sweep-dissolution/lab/src/lib.rs b/fieldtests/cycle-0110-sweep-dissolution/lab/src/lib.rs new file mode 100644 index 0000000..4d913f4 --- /dev/null +++ b/fieldtests/cycle-0110-sweep-dissolution/lab/src/lib.rs @@ -0,0 +1,76 @@ +//! lab — an aura research project: node logic + the exported vocabulary. +//! +//! Every node type this crate provides is registered in `vocabulary()` / +//! `type_ids()` under the `lab::` namespace prefix; the `aura` host loads +//! this cdylib per invocation and merges it with the std vocabulary. + +use aura_core::{ + Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, + ScalarKind, +}; + +/// One-input f64 pass-through. Emits `None` until its input has a value. +pub struct Identity { + out: [Cell; 1], +} + +impl Identity { + pub fn new() -> Self { + Self { out: [Cell::from_f64(0.0)] } + } + pub fn builder() -> PrimitiveBuilder { + PrimitiveBuilder::new( + "lab::Identity", + NodeSchema { + inputs: vec![PortSpec { + kind: ScalarKind::F64, + firing: Firing::Any, + name: "value".into(), + }], + output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], + params: vec![], + }, + |_| Box::new(Identity::new()), + ) + } +} + +impl Default for Identity { + fn default() -> Self { + Self::new() + } +} + +impl Node for Identity { + fn lookbacks(&self) -> Vec { + vec![1] + } + fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> { + let w = ctx.f64_in(0); + if w.is_empty() { + return None; + } + self.out[0] = Cell::from_f64(w[0]); + Some(&self.out) + } + fn label(&self) -> String { + "lab::Identity".to_string() + } +} + +fn vocabulary(type_id: &str) -> Option { + match type_id { + "lab::Identity" => Some(Identity::builder()), + _ => None, + } +} + +fn type_ids() -> &'static [&'static str] { + &["lab::Identity"] +} + +aura_core::aura_project! { + namespace: "lab", + vocabulary: vocabulary, + type_ids: type_ids, +}