From bf600334e97c47e44a6b04c8f0e7c71e0af64e54 Mon Sep 17 00:00:00 2001 From: Brummel Date: Wed, 24 Jun 2026 13:49:50 +0200 Subject: [PATCH] export-runner: classify a completed-but-empty backtest as a data hole, not a failure The first full production run aborted on Copper 2017-04: a month with no data *inside* the symbol's range. cTrader does not emit the clean "No historical data" message for an in-range hole; it runs the backtest to its `}` summary but with a degenerate empty result and no EXPORT_SUCCESS. run_one misread that as a transient failure and hard-aborted after retries. Reaching the `}` summary means the backtest completed cleanly, so the absence of an export marker is an empty month (rc 3, skipped), not a transient failure. A genuine transient failure never reaches `}`, which is what distinguishes the two. --- docs/export-runner-quirks.md | 10 +++++++++- scripts/export-runner.sh | 8 +++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/export-runner-quirks.md b/docs/export-runner-quirks.md index 56aad3a..302007b 100644 --- a/docs/export-runner-quirks.md +++ b/docs/export-runner-quirks.md @@ -24,7 +24,15 @@ outcome, instead of trusting a single stdout sentinel: - `DATA_FOUND` / `EXPORT_SUCCESS` → **success** (rc 0) - `No historical data for the specified period` → **clean-empty** (rc 3, skipped) -- anything else — timeout, `ERROR:`, a result block with no verdict, a dead stream → **transient failure** (rc 2, retried then aborted) +- the backtest reaches its `}` summary but exported nothing → **clean-empty** (rc 3, skipped). + This catches a cTrader-side **data hole** (a month with no data *inside* a symbol's + range, e.g. Copper 2017-04), which emits a degenerate empty result block + (`{ "Equity":, … }`) instead of the clean "No historical data" message — surfaced by + the first real production run. +- bot `ERROR:` (an OnStop exception, e.g. a write failure) → **failure** (rc 2) +- no `}` terminator at all — timeout, crash, dead stream → **transient failure** (rc 2, + retried then aborted). A genuine transient failure never reaches the clean `}` summary, + which is what separates it from a data hole. A produced ZIP is validated (`testzip` + non-empty entry) **before** the `mv`, so a truncated artifact from a cut-off run is caught instead of being promoted to a final file. diff --git a/scripts/export-runner.sh b/scripts/export-runner.sh index 8d00a50..ec84968 100755 --- a/scripts/export-runner.sh +++ b/scripts/export-runner.sh @@ -109,9 +109,11 @@ run_one() { docker rm -f "$C_NAME" >/dev/null 2>&1 CURRENT_CONTAINER="" - if (( saw_success )); then return 0 - elif (( saw_empty )); then return 3 - else return 2 # saw_error, no terminator (timeout/stream died), or `}` without a verdict + if (( saw_success )); then return 0 # DATA_FOUND / EXPORT_SUCCESS + elif (( saw_error )); then return 2 # bot raised an exception (e.g. write failure) -> retry/abort + elif (( saw_empty )); then return 3 # console: "No historical data for the specified period" + elif (( saw_term )); then return 3 # backtest reached its `}` summary but exported nothing -> data hole / empty month + else return 2 # no terminator: timeout / crash / dead stream -> transient failure fi }