90c1de841d
Public-API research deep-dive driving the just-shipped GER40 real-data
session-breakout demo (5b5f034) through the escalating sequence a serious
researcher attempts next: scale to multi-year, walk-forward, structural-param
sweep, multi-instrument compare. Standalone consumer crate (path-deps only;
public interface = ledger + glossary + example corpus + cargo doc; no
crates/*/src read). All runs against the real /mnt/tickdata/Pepperstone archive.
Findings: 3 bugs, 1 friction, 3 spec_gap, 1 working. Triaged to the tracker:
- spec_gap: the flagship breakout ships as a raw FlatGraph the World API cannot
consume (sweep / walk_forward / compare all forced a hand re-author to a
Composite, which reproduced the FlatGraph numbers EXACTLY) — refs #94.
- bug: process residency is O(window) (6->173 MiB across 1mo->10y) despite the
documented O(one-chunk) claim; the aura ring stays <=1024 but data-server
retention scales, and streaming_seam.rs only measures the ring — refs #95.
- bug: the bar-period is one knob split across Resample (tunable param) and
Session (baked) and desyncs silently to 0.0 pips; Delay.lag leaks into the
default param_space (the C34 deform-vs-tune discriminator) — refs #96.
- spec_gap: the walk_forward empty-space idiom for param-free strategies is
undefined — refs #97.
- spec_gap (no per-instrument pip registry) and friction (one-directional
ns->ms conversion) re-confirm the known #22 and #80.
- working: C1 determinism over 7y / 148k bars, the lazy streaming source, and
the sweep/WFO cores held under real multi-year load; the Composite re-author
was behaviour-preserving (C23).
Scratch consumer crate + run transcripts committed under fieldtests/ per the
fieldtest convention (matching cycle-0049). The crate path-deps the engine and
reads the real archive; its bins are the repros cited in the issues above.
134 lines
5.6 KiB
Rust
134 lines
5.6 KiB
Rust
// Task 2 — Walk-forward the SHIPPED breakout across real GER40 history.
|
|
//
|
|
// The researcher's task: "the engine advertises a World walk-forward family.
|
|
// Roll my GER40 breakout across 2018..2024 with in-sample/out-of-sample windows
|
|
// and stitch the OOS pip curve." The breakout I want to roll is the SHIPPED one
|
|
// (crates/aura-ingest/examples/shared/breakout_real.rs) — a hand-wired FlatGraph.
|
|
//
|
|
// This file records the discovery journey verbatim. The public walk_forward is:
|
|
// walk_forward(roller: WindowRoller, space: Vec<ParamSpec>,
|
|
// run_window: Fn(WindowBounds) -> WindowRun) -> WalkForwardResult
|
|
// with WindowRun { chosen_params: Vec<Cell>, oos_equity, oos_report }.
|
|
//
|
|
// SEAM 1 (recorded, not worked around): the breakout has NO swept params in this
|
|
// scenario (a fixed-param OOS roll), yet walk_forward DEMANDS a space:
|
|
// Vec<ParamSpec> and each WindowRun a chosen_params: Vec<Cell>. With a raw
|
|
// FlatGraph there is no param_space() to hand it — the FlatGraph is built by
|
|
// Harness::bootstrap, which takes no params. So I pass an EMPTY space (vec![]) and
|
|
// EMPTY chosen_params (vec![]). Whether that is the intended idiom for a
|
|
// param-free strategy is a spec question — recorded.
|
|
//
|
|
// SEAM 2 (recorded): the breakout source is FOUR M1FieldSources (OHLC) opened by
|
|
// Unix-ms, but WindowBounds.{is,oos} are epoch-ns. aura-ingest exports
|
|
// unix_ms_to_epoch_ns but NOT its inverse, so I hand-divide ns->ms (the milestone
|
|
// fixture's mw_3 hit the same seam).
|
|
//
|
|
// PUBLIC SURFACE ONLY: walk_forward signature from cargo doc; the breakout wiring
|
|
// reused verbatim from the shipped example's shared module.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use chrono::TimeZone;
|
|
use data_server::{DataServer, DEFAULT_DATA_PATH};
|
|
|
|
use aura_core::Timestamp;
|
|
use aura_engine::{
|
|
walk_forward, RollMode, RunReport, WindowBounds, WindowRoller, WindowRun,
|
|
};
|
|
|
|
#[path = "../../crates/aura-ingest/examples/shared/breakout_real.rs"]
|
|
mod breakout_real;
|
|
use breakout_real::*;
|
|
|
|
// SEAM 2: ns -> ms (aura-ingest exports only the forward direction).
|
|
fn ns_to_ms(ts: Timestamp) -> i64 {
|
|
ts.0 / 1_000_000
|
|
}
|
|
|
|
// Run the SHIPPED breakout over one [from, to] epoch-ns window of real GER40 M1
|
|
// OHLC, returning the OOS pip-equity stream + the folded RunReport.
|
|
fn run_breakout_window(
|
|
server: &Arc<DataServer>,
|
|
from: Timestamp,
|
|
to: Timestamp,
|
|
) -> (Vec<(Timestamp, f64)>, RunReport) {
|
|
let from_ms = ns_to_ms(from);
|
|
let to_ms = ns_to_ms(to);
|
|
// Open the four OHLC field sources directly (the shipped open_ohlc_sources
|
|
// helper hard-codes Some(from)/Some(to) and returns Option — reuse it).
|
|
let sources = open_ohlc_sources(server, from_ms, to_ms).expect("window overlaps GER40 data");
|
|
let (mut h, taps) = build_harness();
|
|
h.run(sources);
|
|
let trace = drain_trace(&taps);
|
|
let report = report_from_trace(&trace, from_ms, to_ms);
|
|
let oos_equity: Vec<(Timestamp, f64)> = trace.iter().map(|b| (b.ts, b.equity)).collect();
|
|
(oos_equity, report)
|
|
}
|
|
|
|
fn main() {
|
|
let server = Arc::new(DataServer::new(DEFAULT_DATA_PATH));
|
|
if !server.has_symbol(SYMBOL) {
|
|
println!("skip: no local {SYMBOL} data");
|
|
return;
|
|
}
|
|
|
|
println!("=== Task 2: walk-forward the shipped GER40 breakout (2018..2024) ===\n");
|
|
|
|
// The roll span: 2018-01 .. 2024-12 in epoch-ns. is=12mo, oos=3mo, step=3mo.
|
|
let day_ns: i64 = 86_400_000 * 1_000_000;
|
|
let month_ns: i64 = 30 * day_ns;
|
|
let origin = aura_ingest::unix_ms_to_epoch_ns(
|
|
chrono::Utc.with_ymd_and_hms(2018, 1, 1, 0, 0, 0).unwrap().timestamp_millis(),
|
|
);
|
|
let end = aura_ingest::unix_ms_to_epoch_ns(
|
|
chrono::Utc.with_ymd_and_hms(2024, 12, 31, 23, 59, 59).unwrap().timestamp_millis(),
|
|
);
|
|
let roller = WindowRoller::new(
|
|
(origin, end),
|
|
12 * month_ns, // in-sample length
|
|
3 * month_ns, // out-of-sample length
|
|
3 * month_ns, // step
|
|
RollMode::Rolling,
|
|
)
|
|
.expect("span fits >= 1 window");
|
|
|
|
let server_for_closure = Arc::clone(&server);
|
|
|
|
// SEAM 1: the breakout is param-free here, so space = vec![] and each
|
|
// WindowRun carries chosen_params = vec![]. There is no in-sample optimize to
|
|
// do (no params to tune with a raw FlatGraph), so each window just runs the
|
|
// fixed strategy on its OOS slice. (Tuning the breakout's structural params is
|
|
// Task 3 — and the seam there is that they are NOT a FlatGraph param_space.)
|
|
let result = walk_forward(roller, vec![], move |w: WindowBounds| -> WindowRun {
|
|
let (oos_equity, oos_report) =
|
|
run_breakout_window(&server_for_closure, w.oos.0, w.oos.1);
|
|
WindowRun { chosen_params: vec![], oos_equity, oos_report }
|
|
});
|
|
|
|
println!("walk-forward produced {} windows\n", result.windows.len());
|
|
for (k, w) in result.windows.iter().enumerate() {
|
|
let b = w.bounds;
|
|
println!(
|
|
" window {k}: is=[{}..{}] oos=[{}..{}] oos_pips={:>8.1} oos_dd={:>7.1}",
|
|
ns_to_ms(b.is.0),
|
|
ns_to_ms(b.is.1),
|
|
ns_to_ms(b.oos.0),
|
|
ns_to_ms(b.oos.1),
|
|
w.run.oos_report.metrics.total_pips,
|
|
w.run.oos_report.metrics.max_drawdown,
|
|
);
|
|
}
|
|
|
|
let stitched = &result.stitched_oos_equity;
|
|
println!(
|
|
"\nstitched OOS curve: {} points, final cumulative = {:.1} pips",
|
|
stitched.len(),
|
|
stitched.last().map(|(_, v)| *v).unwrap_or(0.0),
|
|
);
|
|
|
|
// The stitched OOS curve is the breakout's out-of-sample pip total over the
|
|
// concatenated OOS quarters (no in-sample optimization happened — same fixed
|
|
// FlatGraph strategy everywhere, since a raw FlatGraph has no param_space()).
|
|
println!("\nOK: fixed-param OOS walk-forward over the shipped breakout FlatGraph.");
|
|
}
|