cd6874b43f
The price files carry no facts for interpreting the bars (pip size, digit count, contract size, quote currency); those are known only to the broker via cTrader's Symbol object. Produce a JSON sidecar per symbol next to the price files so a downstream reader does not have to hardcode (and drift on) its own per-symbol table. Scope is the price-interpretation geometry only — digits, pipSize, tickSize, lotSize, baseAsset, quoteAsset — facts intrinsic to the price series and stable across its whole history. Costs, swaps, volume limits, trading status and the deposit-currency pip/tick values are intentionally left out: the cBot only ever sees the current contract, so those are a point-in-time snapshot of today's trading conditions and would misrepresent a multi-year series (the deposit-currency values are account-dependent too). - cBot: a MetaOnly mode (--MetaOnly --MetaFile) serializes the geometry to a flat JSON. Numbers use InvariantCulture; a non-finite value becomes null; keys mirror the cTrader property names. - runner: write_meta runs once per symbol (mode-independent), validates the JSON, and atomically promotes the sidecar. Best-effort — a metadata failure is logged and never aborts the export, and a pre-existing sidecar is kept on failure. A freshness guard (META_MAX_AGE_DAYS, default 7) keeps scheduled runs short; EXPORT_META=0 disables it. - The file is a raw serialization of what cTrader delivers; a reader derives its own shape (contract_size, per-lot pip value) from the raw fields. See docs/symbol-metadata.md. - Add a source-level regression test for the freshness guard. Verified end-to-end against the live broker for all 30 configured symbols (FX, indices, metals, crypto, commodities, equities); the price corpus was untouched.
91 lines
4.0 KiB
Bash
Executable File
91 lines
4.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Source-level unit tests for export-runner.sh. The runner is written to be sourceable
|
|
# (main runs only when executed directly), so we can load its functions, stub the parts
|
|
# that would spawn containers, and assert pure logic. No Docker, no broker, no data dir.
|
|
#
|
|
# Run: scripts/test-export-runner.sh (exit 0 = all pass)
|
|
set -uo pipefail
|
|
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
RUNNER="$HERE/export-runner.sh"
|
|
|
|
# The runner sources runtime/export.env and hard-requires HOST_DATA_PATH/CTID/ACCOUNT.
|
|
# Provide throwaway values so sourcing succeeds without the real deployment config.
|
|
export ENV_FILE; ENV_FILE="$(mktemp)"
|
|
cat > "$ENV_FILE" <<'ENV'
|
|
HOST_DATA_PATH=/nonexistent
|
|
CTID=test@example.com
|
|
ACCOUNT=0
|
|
ENV
|
|
trap 'rm -f "$ENV_FILE"' EXIT
|
|
|
|
# shellcheck disable=SC1090
|
|
source "$RUNNER"
|
|
|
|
PASS=0 FAIL=0
|
|
check() { # check NAME EXPECTED ACTUAL
|
|
if [[ "$2" == "$3" ]]; then echo " ✓ $1"; PASS=$((PASS+1))
|
|
else echo " ✗ $1 — expected [$2], got [$3]"; FAIL=$((FAIL+1)); fi
|
|
}
|
|
|
|
# --- discover_start contamination regression -------------------------------------------
|
|
# run_one streams "[BOT] …" progress on STDOUT; discover_start's STDOUT is its result
|
|
# channel (read via command substitution). A stub that mimics that noise must NOT leak
|
|
# into the result. Pre-fix this returned garbage like "0-000-…" -> start_idx 0 -> the
|
|
# year-0000 "--start=01/01/0" abort. CURRENT_YEAR/MONTH come from the host clock; pick a
|
|
# data-start strictly before today so the assertion is stable regardless of the date.
|
|
emit_noise() { echo " [BOT] Progress 50% | bla"; echo " [BOT] | Info | noise"; }
|
|
|
|
# Scenario 1: data starts in a past December (yearly probe hits), Jan of that year empty,
|
|
# Feb has data -> expect "<year> 2".
|
|
TARGET_YEAR=$(( CURRENT_YEAR - 1 ))
|
|
probe_run() { emit_noise
|
|
case "$3" in
|
|
"01/12/$TARGET_YEAR") return 0 ;; # yearly probe: this December has data
|
|
"01/02/$TARGET_YEAR") return 0 ;; # monthly probe: February is the first month
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
got="$(discover_start FOO M1)"
|
|
check "discover_start: result is clean (no [BOT] leakage)" "$TARGET_YEAR 2" "$got"
|
|
|
|
# Scenario 2: no data in any past year, first data in an early month of the current year.
|
|
probe_run() { emit_noise
|
|
[[ "$3" == "01/0$(( CURRENT_MONTH > 1 ? CURRENT_MONTH - 1 : CURRENT_MONTH ))/$CURRENT_YEAR" ]] && return 0
|
|
return 1
|
|
}
|
|
got="$(discover_start FOO M1)"
|
|
# first month of the current year that the stub reports data for:
|
|
want_m=$(( CURRENT_MONTH > 1 ? CURRENT_MONTH - 1 : CURRENT_MONTH ))
|
|
check "discover_start: current-year fallback, clean result" "$CURRENT_YEAR $want_m" "$got"
|
|
|
|
# Scenario 3: genuinely no data anywhere -> empty stdout, rc 1.
|
|
probe_run() { emit_noise; return 1; }
|
|
got="$(discover_start FOO M1)"; rc=$?
|
|
check "discover_start: no data -> empty result" "" "$got"
|
|
check "discover_start: no data -> rc 1" "1" "$rc"
|
|
|
|
# --- month index round-trip (sanity on the arithmetic the resume loop depends on) ------
|
|
i=$(midx 2026 5)
|
|
check "midx/idx_year round-trip" "2026" "$(idx_year "$i")"
|
|
check "midx/idx_month round-trip" "5" "$(idx_month "$i")"
|
|
|
|
# --- write_meta freshness guard (no Docker needed) -------------------------------------
|
|
# A recent sidecar must be skipped without spawning a container, so scheduled runs stay
|
|
# short. Point HOST_DATA_PATH at a temp dir, drop a just-created meta file, and assert
|
|
# write_meta classifies it as a fresh-skip. A docker stub fails loudly if it wrongly runs.
|
|
TMPDATA="$(mktemp -d)"; HOST_DATA_PATH="$TMPDATA"
|
|
: > "$TMPDATA/EURUSD.meta.json" # mtime = now -> inside any positive window
|
|
META_OK=0; META_FAIL=0; META_SKIP=0; META_MAX_AGE_DAYS=7
|
|
docker() { echo "UNEXPECTED docker call" >&2; return 1; } # must NOT be reached on a fresh file
|
|
write_meta EURUSD >/dev/null 2>&1
|
|
unset -f docker
|
|
check "write_meta: fresh sidecar -> skipped" "1" "$META_SKIP"
|
|
check "write_meta: fresh sidecar -> not produced" "0" "$META_OK"
|
|
rm -rf "$TMPDATA"
|
|
|
|
echo "-----"
|
|
echo "$PASS passed, $FAIL failed"
|
|
[[ $FAIL -eq 0 ]]
|