Files
foxhunt/scripts/audit-diag.sh
jgrusewski 40855bfd62 docs(sp20): trader-grade trade management spec + audit infrastructure
Adds SP20 — full production trader-management system in one
greenfield commit (3-4 weeks of implementation work to follow):

  * Tier 0: multi-resolution time-scaled market features (3 horizons)
  * Tier 1: trade-arc awareness (4 features per batch)
  * Tier 2: per-unit trail-stop (entry + trail + stop per unit)
  * Tier 3: pyramiding + partial profit-taking (HalfFlat actions,
            N_ACTIONS=9→11)
  * Tier 4: Forward-Return-Distribution head + confidence gate +
            per-batch anti-martingale sizing + position heat cap +
            vol-adjusted defaults

Spec went through critical-review pass (v1→v2→v3):
  * v1: 3 tiers, side-channel features, single-gate acceptance
  * v2: 5 tiers added partial-flat + anti-mart + multi-res + checklist
  * v3: foundational fixes for 4 CRIT + 6 SIG + 6 MIN findings
        (per-unit pyramid state, encoder-input injection vs side-channel,
         FRD head replaces survivor-biased checklist, override stack
         ordering, per-batch anti-mart, real-time multi-res scales,
         P-1 ceiling falsification gate, multi-tier acceptance)

§0 Foundational Principles (NEW, non-negotiable):
  * §0.1 every numerical constant ISV-resident (no hardcoded #defines
         in new kernels; structural-dim exception only)
  * §0.2 every kernel/slot/head/action fully wired in same commit
  * §0.3 diagnostics baked in at birth (every observable in JSONL)
  * §0.4 per-phase ship-gate: all three audits must pass

Audit infrastructure shipped with the spec:
  * scripts/audit-isv.sh      — greps new .cu for hardcoded #defines
  * scripts/audit-wiring.sh   — verifies kernels/slots/heads/actions
                                have producer + consumer in code
  * scripts/audit-diag.sh     — runs local 100-step smoke, validates
                                manifest-listed jq paths present in JSONL
  * scripts/audit-manifest/   — per-phase append manifests (kernels,
                                slots, heads, actions, diag-fields)

Naming discipline: audit scripts and manifest are SP-agnostic (no
`sp20-` prefix) per new pearl `feedback_no_sp_or_version_prefixes_in_file_names`
— they'll serve future SPs too. SP numbers belong only in
docs/superpowers/{specs,plans}/ filenames.

Audit scripts dogfooded — already caught two real violations on
existing code that the formal review missed:
  * audit-isv: KL_EMA_ALPHA=0.05f hardcoded in rl_q_pi_distill_grad.cu
  * audit-wiring: TrailTighten action (a7) has no handler in any
                  kernel (per pearl_dead_trail_stop_actions_a7_a8)

These violations are SP20 P5/P10 fix targets.

User decision recorded in spec §3 P-1: ceiling-falsification phase
intentionally skipped — SP20 is the architectural launchpad for
the broader trader system regardless of whether current arch could
be pushed further at 1M steps. P-1 may be revisited as standalone
work after SP20 ships.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 16:28:39 +02:00

138 lines
4.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# audit-diag.sh — enforce "every observable surfaces in diag JSONL"
# per SP20 §0.3.
#
# Runs a local 100-step smoke (small footprint, b_size=4, predecoded
# test data) and parses the produced diag JSONL. For each entry in
# scripts/audit-manifest/diag-fields.txt: verify the jq-syntax path
# resolves to a non-null, non-NaN value in at least one sampled row.
#
# Exit codes:
# 0 — zero missing fields
# 1 — one or more missing fields
# 2 — usage / smoke-run failure
set -euo pipefail
ROOT=$(git rev-parse --show-toplevel)
MANIFEST="$ROOT/scripts/audit-manifest/diag-fields.txt"
BINARY="$ROOT/target/release/examples/alpha_rl_train"
SMOKE_OUT_DIR="${TMPDIR:-/tmp}/audit-diag-smoke-$$"
DIAG_FILE="$SMOKE_OUT_DIR/diag.jsonl"
if ! command -v jq >/dev/null 2>&1; then
echo "ERROR: jq required (apt install jq)" >&2
exit 2
fi
if [[ ! -f "$MANIFEST" ]]; then
echo "ERROR: manifest not found: $MANIFEST" >&2
exit 2
fi
mapfile -t fields < <(grep -vE '^\s*(#|$)' "$MANIFEST")
if [[ ${#fields[@]} -eq 0 ]]; then
echo "audit-diag: manifest empty — no diag fields to audit. PASS."
exit 0
fi
# ── Build binary if missing/stale ─────────────────────────────
if [[ ! -x "$BINARY" || "$BINARY" -ot "$ROOT/crates/ml-alpha/src/trainer/integrated.rs" ]]; then
echo "audit-diag: building alpha_rl_train release binary..."
(cd "$ROOT" && SQLX_OFFLINE=true cargo build -p ml-alpha --example alpha_rl_train --release 2>&1 | tail -5)
fi
# ── Local smoke: 100 steps, b_size=4, single fold, single seed ───
mkdir -p "$SMOKE_OUT_DIR"
echo "audit-diag: running 100-step local smoke (b_size=4) → $SMOKE_OUT_DIR"
# Inputs — use test-data MBP-10 baseline
TEST_DATA="${FOXHUNT_TEST_DATA:-$ROOT/test_data/futures-baseline}"
if [[ ! -d "$TEST_DATA/ES.FUT" ]]; then
echo "ERROR: test data missing: $TEST_DATA/ES.FUT" >&2
echo "Set FOXHUNT_TEST_DATA env var to a directory containing ES.FUT/ subdirectory" >&2
exit 2
fi
PREDECODED="$TEST_DATA/predecoded"
if [[ ! -d "$PREDECODED" ]]; then
PREDECODED="$TEST_DATA"
fi
"$BINARY" \
--mbp10-data-dir "$TEST_DATA/ES.FUT" \
--trades-data-dir "$TEST_DATA/ES.FUT" \
--predecoded-dir "$PREDECODED" \
--output-dir "$SMOKE_OUT_DIR" \
--n-steps 100 \
--n-backtests 4 \
--seq-len 32 \
--per-capacity 256 \
--seed 16962 \
--instrument-mode front-month \
--n-folds 1 \
--fold-idx 0 \
--n-eval-steps 0 \
> "$SMOKE_OUT_DIR/stdout.log" 2> "$SMOKE_OUT_DIR/stderr.log" \
|| { echo "ERROR: local smoke failed; see $SMOKE_OUT_DIR/stderr.log"; tail -20 "$SMOKE_OUT_DIR/stderr.log"; exit 2; }
if [[ ! -f "$DIAG_FILE" ]]; then
echo "ERROR: smoke completed but diag.jsonl not produced at $DIAG_FILE" >&2
exit 2
fi
n_rows=$(wc -l < "$DIAG_FILE")
echo "audit-diag: smoke produced $n_rows diag rows"
if [[ $n_rows -lt 10 ]]; then
echo "ERROR: too few diag rows ($n_rows); smoke may have crashed early" >&2
exit 2
fi
# Sample 3 rows: first, middle, last
mid_row=$((n_rows / 2))
last_row=$n_rows
sample_rows=(1 $mid_row $last_row)
# ── Verify each manifest field present + non-trivial ─────────
violations=0
echo
echo "audit-diag: validating ${#fields[@]} field path(s)..."
for field in "${fields[@]}"; do
found_any=0
bad_any=0
for r in "${sample_rows[@]}"; do
val=$(sed -n "${r}p" "$DIAG_FILE" | jq -c "$field // empty" 2>/dev/null || echo "<jq-error>")
if [[ "$val" == "<jq-error>" ]]; then
bad_any=1
continue
fi
if [[ -n "$val" && "$val" != "null" ]]; then
# Check for NaN (jq emits null for NaN typically; double-check)
if [[ "$val" != *"NaN"* && "$val" != *"nan"* ]]; then
found_any=1
fi
fi
done
if [[ $found_any -eq 0 ]]; then
echo " VIOLATION field $field: not present (or null/NaN) in sampled rows"
violations=$((violations + 1))
fi
done
echo
echo "audit-diag: $violations missing field(s)"
if [[ $violations -gt 0 ]]; then
echo
echo "Per SP20 §0.3: every observable state MUST be in diag JSONL same commit as the mechanic."
echo "Add the field to the diag JSONL emission in crates/ml-alpha/examples/alpha_rl_train.rs"
exit 1
fi
echo "audit-diag: PASS (sampled rows: ${sample_rows[*]})"
# Cleanup smoke dir on success
rm -rf "$SMOKE_OUT_DIR"
exit 0