From 3dc02b3072345ae207c7b76cba2de3d09e581afe Mon Sep 17 00:00:00 2001 From: danielfmonzon <123423019+danielfmonzon@users.noreply.github.com> Date: Sat, 15 Aug 2026 13:59:40 -0400 Subject: [PATCH 1/3] phase 3: AI improvement pipeline, path anchoring, env-secret gate The pipeline: propose -> firewall -> implement -> human merge. * improve/sources.py - allowlist of what analysis may READ (published artifacts and the decision log; source code is deliberately not readable evidence). * improve/firewall.py - denylist of what a proposal may CHANGE: seven frozen paths and six change classes, enforced at propose time AND re-run against the actual diff at implement time. Refusals cite the iron rule verbatim. * improve/propose.py - writes docs/proposals/PROP-n only; never edits code. * improve/implement.py - branch, apply, gate, report, push, STOP. No merge verb exists in the module; two tests prove main is never touched. Path anchoring (Defect #1 and its two siblings found by the audit): every module-level default path in glassbox/ now derives from PROJECT_ROOT rather than the CWD. The unattended chain runs in C:\Windows\System32, where a relative Path("reports") raised PermissionError WinError 5 naming a directory that had never existed. verify_dist s .env fallback was the same defect but failed worse: a missing .env is not an error there, so the env-secret search would have silently become a no-op while the gate still reported PASS. Env-secret gate (consequence of Defect #2): in the automated refresh chain, an UNCHECKED env-secret status now aborts at the deploy decision exactly as a redaction does. Interactive and --dry-run keep the note. 668 tests pass; ruff and mypy clean. --- docs/decisions.md | 181 +++++++++++++ src/quantlab/cli.py | 155 ++++++++++- src/quantlab/glassbox/refresh.py | 63 ++++- src/quantlab/glassbox/serve.py | 8 +- src/quantlab/glassbox/snapshot.py | 15 +- src/quantlab/glassbox/verify_dist.py | 19 +- src/quantlab/improve/__init__.py | 18 ++ src/quantlab/improve/firewall.py | 321 +++++++++++++++++++++++ src/quantlab/improve/implement.py | 370 +++++++++++++++++++++++++++ src/quantlab/improve/propose.py | 216 ++++++++++++++++ src/quantlab/improve/sources.py | 126 +++++++++ tests/test_glassbox_refresh.py | 100 +++++++- tests/test_improve_firewall.py | 172 +++++++++++++ tests/test_improve_pipeline.py | 286 +++++++++++++++++++++ tests/test_path_anchoring.py | 128 +++++++++ 15 files changed, 2166 insertions(+), 12 deletions(-) create mode 100644 src/quantlab/improve/__init__.py create mode 100644 src/quantlab/improve/firewall.py create mode 100644 src/quantlab/improve/implement.py create mode 100644 src/quantlab/improve/propose.py create mode 100644 src/quantlab/improve/sources.py create mode 100644 tests/test_improve_firewall.py create mode 100644 tests/test_improve_pipeline.py create mode 100644 tests/test_path_anchoring.py diff --git a/docs/decisions.md b/docs/decisions.md index cd49dc6..6c7f728 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -6,6 +6,187 @@ compiled on 2026-07-10 (v1.0.0). Newest entries first. --- +## 2026-08-15 — An unchecked secret gate is an abort, not a note (automated chain only) + +**Decision.** In the **automated** refresh chain, an env-secret status of NOT CHECKED +aborts at the deploy decision exactly as a redaction does: bytes held, WARNING raised, +human pre-review required. Interactive runs (`--interactive`) and `--dry-run` keep the +existing note and proceed. + +**Rationale — direct consequence of Defect #2** (the 2026-08-15 path-anchoring audit). +`verify_dist`'s `.env` fallback was CWD-relative, and a missing `.env` is not an error in +`load_env_secret_prefixes`: it returns an empty prefix set with a note and the gate still +reports **PASS**. Under the scheduler, which supplies no working directory, that +combination would have published bytes whose env-secret half had searched for nothing +while the chain report said the gate passed. The path bug is fixed; this closes the +class. **"The check silently did not run" must never be indistinguishable from "the check +ran and found nothing"** — that is the same failure shape as Defect #1, where an error +naming `reports` described a directory that had never existed. + +**Why the two modes differ.** The abort exists to substitute for a reader who is absent, +so it fires exactly when the reader is. `--dry-run` publishes nothing and an interactive +operator sees the note in the report they are already reading. The default is +`automated=True` — fail closed, so a run that did not declare a human is assumed not to +have one. The env-secret status is now printed on **every** report, passing or not, +because "the check ran" should be confirmable rather than inferred from the absence of a +note. + +--- + +## 2026-08-15 — The AI improvement pipeline, and the firewall that makes it safe + +**Decision.** An automated improvement loop is added in three parts: +`quantlab propose` (analysis, writes a document), a structural **firewall** (a denylist +of frozen paths and change classes), and `quantlab implement PROP-n` (applies on a +branch, gates it, reports, pushes, stops). Merge is human-only. The loop may improve the +machine around the strategy; it may never touch the strategy, the limits that constrain +it, the instruments that measure it, or the gate that keeps secrets out of the published +bytes. + +**Why this needs a firewall at all.** The failure mode is not a rogue model, it is a +*reasonable* one. Point an improvement loop at this repository and the most persuasive +proposal available to it is to widen the 50 bps divergence threshold: four of the last +six weekly reviews fired DIVERGING, every one was later attributed to mark-phase +geometry, and the alerts produced no action. That argument is evidenced, coherent, and +exactly wrong — it is the 2026-08-10 ruling ("fix the instrument, not the threshold") +run in reverse. A prompt asking a model not to do that is a request. This is a gate. + +**What `propose` may READ** — an allowlist, in `improve/sources.py`: weekly reviews, +daily digests, per-run paper reports, `alerts.jsonl`, the CI workflow, Glass Box / site +artifacts, `decisions.md`, and prior proposals. **Source code is deliberately not +readable evidence.** An observation has to trace to an artifact this system published +about itself; a proposal justified by reading the implementation is how you end up +"fixing" a measurement to agree with the code rather than the other way round. Absent +sources are reported as absent rather than silently skipped. + +**What no proposal may CHANGE** — the firewall, in `improve/firewall.py`, enforced at +BOTH ends. Forbidden paths: + +| path | why | +|---|---| +| `src/quantlab/backtest/strategies/` | strategy definitions and literature-fixed parameters | +| `src/quantlab/broker/` | the order path, frozen under human review | +| `src/quantlab/risk/` | limits, kill-switch, halt state | +| `config/risk.yaml`, `config/crypto_risk.yaml` | risk limit values | +| `src/quantlab/glassbox/sanitize.py` | sanitizer patterns — the secret-leak gate | +| `src/quantlab/scheduling/tasks.py` | schedule cadences | + +Paths alone are not enough — a threshold can be moved from a constant in a reporting +module — so six forbidden **change classes** are also matched against the proposal's own +prose: `strategy_parameters`, `risk_limits`, `threshold_constants`, `schedule_cadence`, +`sanitizer_patterns`, `broker_logic`. A class fires only when a frozen TARGET and a +mutation VERB co-occur, because a target alone is discussion and a verb alone is most of +English. Matching is word-boundary anchored: naive substring matching fired `change` +inside "un**change**d" and refused the sentence "the threshold value itself is +unchanged" — a firewall that fires on its own disclaimers trains its operator to route +around it, the same lesson as the 2026-07-26 narrowing of `apca_api_header`. + +**Refusals cite the iron rule verbatim** and state that no flag overrides them, because +a refusal that does not explain itself is indistinguishable from a bug. A refused +proposal writes **nothing** — the gate runs before the file is created, mirroring the +snapshot writer's "nothing is written unless the gate passes". + +**Strategy-performance data may inform INFRASTRUCTURE proposals only.** The rule is +enforced on the proposal's EFFECT, not on what it read. Reading the divergence figures +to notice that the Glass Box exposes only the raw number and not the residual the verdict +was taken on is the intended use. Reading the same figures to argue for a different +lookback is refused. + +**Merge is human-only, and that is structural too.** `implement` creates `prop/{n}`, +applies the change, re-runs the firewall **against the actual diff** (the document and +the diff are different artifacts, and only the second becomes a commit), runs +ruff/mypy/pytest plus the frontend suite and `verify-dist` when the site is touched, +writes an implementation report into the proposal, commits, pushes, and ends. There is +no `git merge`, no `git rebase`, and no push to `main` anywhere in it. Two tests prove +this rather than one: a behavioural test runs the real command against a real repository +and asserts `main`'s SHA is byte-identical afterwards, and a source-level test reads +`implement.py` and fails if a merge verb ever appears in it. The first catches a bug; the +second catches a future feature. **Daniel merges via pull request after Quant Lead +review.** + +A loop that can merge its own work is trusted by construction, and every safety property +downstream of it collapses into "the analysis was right" — the one thing that cannot be +guaranteed. Keeping the last step human means the worst case of a wrong proposal is a +branch nobody merges. + +**Dogfooded on PROP-1.** The first real proposal was the Story page's claim that the +system had caught itself "twice"; there have been three self-caught measurement +incidents (the 2026-07-22 scheduler leak, the 2026-07-25 partial-bar read, and the +2026-08-10 comparator interval dating defect). Proposed, firewall-passed, implemented on +`prop/1`, six gates green, pushed, stopped at the human gate. The run also found two real +defects in the pipeline itself, both fixed with regression tests: `implement` staged +`-A` in patch mode (which would have swept unrelated working-tree changes into a +proposal's commit), and the report written into the proposal claimed +`committed: False` on a run that had committed and pushed, because the report is written +before the commit that carries it. + +--- + +## 2026-08-15 — DMARC enforcement: `p=none` -> `p=quarantine` + +**Decision.** The `_dmarc.danielmonzonautomation.com` TXT record moves from +monitor-only to enforcing: + +``` +v=DMARC1; p=quarantine; sp=quarantine; rua=mailto:; fo=1 +``` + +`sp=quarantine` is stated explicitly rather than left to inherit, so a subdomain +cannot become an unenforced sending path by omission. `rua` and `fo=1` are +unchanged — aggregate reporting continues, and enforcement is not a reason to +stop reading it. + +The `rua` mailbox is elided above rather than transcribed, because this file is +published verbatim through `/api/decisions` and `email_address` is a **forbidden** +pattern in the snapshot gate — not a redaction, a hard refusal. Writing the literal +address here would fail-close the next automated refresh on the gate's own +documentation. The live value is in the zone; it is unchanged from the `p=none` +record it replaced. + +**Rationale — the observation window did its job.** `p=none` existed to answer one +question: does anything legitimate send as this domain that would break under +enforcement? The aggregate reports answer no. **10 of 11** reported messages were +DMARC-aligned. The single failure originated from **AWS** infrastructure with no +alignment on either SPF or DKIM — not a legitimate sender misconfigured, but +exactly the unauthorised use of the domain that a policy is supposed to act on. +Quarantining that message is the correct outcome, not collateral damage. A policy +whose only enforcement effect is the thing you deployed it for has no migration +risk left to buy down by waiting. + +**Preconditions verified in the zone before the change**, since enforcement is only +safe if alignment is actually achievable: MX is Google (`SMTP.GOOGLE.COM`, pref 1), +SPF is `v=spf1 include:_spf.google.com ~all`, and a valid `google._domainkey` +DKIM RSA key is published. Daniel confirmed **Gmail-only sending**; the zone +contents corroborate it — there is no second sending path to break. + +**`quarantine`, not `reject`.** Quarantine is recoverable by the recipient: a +false positive lands in spam where it can still be retrieved. Reject is +unrecoverable and bounces. With one enforcing week of evidence and a +single-digit message sample, the recoverable rung is the honest one. `p=reject` +is a later decision that should be taken on aggregate reports gathered *under* +quarantine, not on reports gathered under `none`. + +**Operational note — Netlify DNS cannot modify a record in place.** The API +exposes `createDnsRecord` and `deleteDnsRecord` and **no update method**, so +"edit the record" is necessarily delete-then-create. Order matters and is not +arbitrary: deleting first leaves a brief window with *no* DMARC record, which +receivers treat as no policy — fail-open, mail flows. Creating first would leave +two `_dmarc` records, and RFC 7489 §6.6.3 requires receivers to apply **no +policy at all** when more than one is present — a worse state that also silently +defeats the change. Delete-then-create is therefore the correct order, and the +record ID changes as a consequence (`6a6645686c76095d7a08d75a` -> +`6a808f7c09c4e9aa73343cf1`). + +**Change protocol, run in full.** MX resolved before and after and compared — +unchanged (`pref=1 smtp.google.com`), with the MX record object itself untouched +(same id `6a2bffbd9e417c2f6941fd77`). Exactly one `_dmarc` record confirmed both +before and after, at the API level and at **all four** authoritative +nameservers (`dns{1..4}.p08.nsone.net`), byte-compared against the intended +value. TTL 3600, so resolver caches carry the old `p=none` for up to an hour; +that staleness is expected and is not a failed change. + +--- + ## 2026-08-10 — One bounded retry, and the list of things that must never get one **The gap.** Any abort ended the attempt until the next scheduled day. A vendor publishing a diff --git a/src/quantlab/cli.py b/src/quantlab/cli.py index 00537db..d9cbb7d 100644 --- a/src/quantlab/cli.py +++ b/src/quantlab/cli.py @@ -62,10 +62,11 @@ from quantlab.glassbox.completeness import ( DEFAULT_MAX_AGE_DAYS as GLASSBOX_MAX_SNAPSHOT_AGE_DAYS, ) - -# Constants only: glassbox/__init__ is lazy, so this does not import FastAPI. from quantlab.glassbox.serve import DEFAULT_PORT as GLASSBOX_DEFAULT_PORT from quantlab.glassbox.serve import DEFAULT_SNAPSHOT_DIR + +# Constants only: glassbox/__init__ is lazy, so this does not import FastAPI. +from quantlab.improve.propose import RISK_CLASSES as PROPOSAL_RISK_CLASSES from quantlab.logging_setup import get_logger from quantlab.paper.runner import ( PaperRunReport, @@ -1165,13 +1166,106 @@ def cmd_glassbox_refresh(args: argparse.Namespace) -> int: # should see a dirty-tree warning before the chain starts, not after it has deployed. # Report-only -- it never blocks (see repo_state). warn_if_unclean(check_repo_state()) - result = refresh(dry_run=args.dry_run, max_age_days=args.max_age_days) + result = refresh( + dry_run=args.dry_run, + automated=not args.interactive, + max_age_days=args.max_age_days, + ) print(result.render()) if not result.ok: return 3 return 0 +def cmd_propose(args: argparse.Namespace) -> int: + """Write a proposal, or refuse. Never edits code. + + Exit 0 on a written proposal, 3 on a firewall refusal, 2 on a bad evidence path. + A refusal is a NORMAL outcome, not a crash -- it is the pipeline working. + """ + from quantlab.improve.propose import Proposal, ProposalRefused, write_proposal + from quantlab.improve.sources import SourceViolation, render_inventory + + if args.sources: + print(render_inventory()) + return 0 + + print("quantlab propose: analysis only -- this command never edits code.\n") + print(render_inventory()) + print() + + try: + proposal = Proposal( + title=args.title, + observation=args.observation, + change=args.change, + affected_paths=list(args.affects), + risk_class=args.risk_class, + test_plan=args.test_plan, + evidence=list(args.evidence), + slug=args.slug, + ) + except ValueError as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 2 + + try: + path = write_proposal(proposal) + except ProposalRefused as exc: + print(exc.verdict.render(), file=sys.stderr) + log.error("proposal_refused", + refusals=[r.identifier for r in exc.verdict.refusals]) + return 3 + except SourceViolation as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 2 + + print(f"proposal written: {path}") + print("\nNext: `quantlab implement " f"{proposal.number}` " + "-- applies on a branch, gates it, pushes, and stops. Merge is human-only.") + log.info("proposal_written", number=proposal.number, path=str(path)) + return 0 + + +def cmd_implement(args: argparse.Namespace) -> int: + """Apply PROP-n on its own branch, gate it, report, push, stop. + + Exit 0 when every gate passed, 3 otherwise. A non-zero exit still leaves the branch + and the report in place: the failure is evidence for the human reviewer, not a + reason to hide the work. + """ + from quantlab.improve.implement import NotOnBranch, implement + + raw = str(args.proposal).upper().removeprefix("PROP-") + try: + number = int(raw) + except ValueError: + print(f"ERROR: not a proposal number: {args.proposal!r}", file=sys.stderr) + return 2 + + print(f"quantlab implement PROP-{number}: branch -> apply -> gate -> report -> push -> STOP") + print(" This command never merges. Daniel merges via PR after Quant Lead review.\n") + + try: + result = implement( + number, + patch=Path(args.patch) if args.patch else None, + push=not args.no_push, + ) + except FileNotFoundError as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 2 + except NotOnBranch as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 3 + + print(result.render()) + print(f"\nreport written into: {result.proposal_path}") + if result.aborted: + return 3 + return 0 if result.ok else 3 + + def cmd_weekly(args: argparse.Namespace) -> int: store = ParquetStore() calendar = TradingCalendar() @@ -1719,6 +1813,13 @@ def build_parser() -> argparse.ArgumentParser: "--dry-run", action="store_true", help="run every gate but stop before deploying", ) + p_gb_refresh.add_argument( + "--interactive", action="store_true", + help=( + "a human is reading this run: an unchecked env-secret gate is reported as a " + "note instead of aborting the deploy (default: automated, which aborts)" + ), + ) p_gb_refresh.add_argument( "--max-age-days", type=int, default=GLASSBOX_MAX_SNAPSHOT_AGE_DAYS, help=( @@ -1728,6 +1829,54 @@ def build_parser() -> argparse.ArgumentParser: ) p_gb_refresh.set_defaults(func=cmd_glassbox_refresh) + # -- the AI improvement pipeline: propose -> firewall -> implement -> human merge -- + p_propose = sub.add_parser( + "propose", + help="analyse published artifacts and write docs/proposals/PROP-n (never edits code)", + ) + p_propose.add_argument("--title", required=True, help="one-line proposal title") + p_propose.add_argument( + "--observation", required=True, + help="what was observed, in prose; cite evidence with --evidence", + ) + p_propose.add_argument("--change", required=True, help="the proposed change") + p_propose.add_argument( + "--affects", action="append", default=[], metavar="PATH", + help="a file the change would touch (repeatable); checked against the firewall", + ) + p_propose.add_argument( + "--risk-class", required=True, choices=list(PROPOSAL_RISK_CLASSES), + help="blast radius of the change", + ) + p_propose.add_argument("--test-plan", required=True, help="how the change is verified") + p_propose.add_argument( + "--evidence", action="append", default=[], metavar="PATH", + help=( + "an artifact supporting the observation (repeatable). Must lie inside the " + "allowed read set; source code is not readable evidence." + ), + ) + p_propose.add_argument("--slug", default="", help="filename slug (derived from title)") + p_propose.add_argument( + "--sources", action="store_true", + help="print the evidence-source inventory and exit without writing", + ) + p_propose.set_defaults(func=cmd_propose) + + p_impl = sub.add_parser( + "implement", + help="apply PROP-n on branch prop/n, gate it, report, push, and stop (never merges)", + ) + p_impl.add_argument("proposal", help="proposal number, e.g. 1 or PROP-1") + p_impl.add_argument( + "--patch", default=None, + help="a git patch to apply; without it, changes already in the working tree are used", + ) + p_impl.add_argument( + "--no-push", action="store_true", help="do everything except push the branch", + ) + p_impl.set_defaults(func=cmd_implement) + return parser diff --git a/src/quantlab/glassbox/refresh.py b/src/quantlab/glassbox/refresh.py index ab23c32..904a71a 100644 --- a/src/quantlab/glassbox/refresh.py +++ b/src/quantlab/glassbox/refresh.py @@ -105,6 +105,10 @@ def status(self) -> str: class RefreshResult(BaseModel): started_at: datetime dry_run: bool = False + # True when this run may publish without a human reading the report first. Only an + # automated run treats an UNCHECKED env-secret gate as an abort; see the deploy + # decision for why the two modes differ. + automated: bool = True steps: list[StepOutcome] = [] # The snapshot writer's report, then the published-bytes gate's report. Both are # rendered into the email; both feed the deploy decision. @@ -113,6 +117,18 @@ class RefreshResult(BaseModel): redaction_count: int = 0 redactable_findings: list[str] = [] forbidden_matches: list[str] = [] + # Whether each half of the secret search actually ran. A missing or unreadable `.env` + # is not an error inside `load_env_secret_prefixes` — it returns an empty prefix set + # with a note and the surrounding gate still PASSES. That is tolerable when a human + # is reading the report and can see the note; it is not tolerable when the chain + # deploys on its own. See the deploy decision. + snapshot_env_checked: bool = True + dist_env_checked: bool = True + env_notes: list[str] = [] + + @property + def env_checked(self) -> bool: + return self.snapshot_env_checked and self.dist_env_checked deployed: bool = False deploy_url: str | None = None aborted_at: str | None = None @@ -162,6 +178,17 @@ def render(self) -> str: f"{'(must be 0 to auto-deploy)' if self.redaction_count else 'ok'}") lines.append(f" redactable found : {len(self.redactable_findings)} " f"{'(must be 0 to auto-deploy)' if self.redactable_findings else 'ok'}") + # Stated on every report, not only when it fails: "the check ran" is a claim the + # reader should be able to confirm rather than assume from the absence of a note. + if self.env_checked: + env_status = "ran" + elif self.automated and not self.dry_run: + env_status = "NOT CHECKED (must have run to auto-deploy)" + else: + env_status = "NOT CHECKED (note only — no automated deploy in this mode)" + lines.append(f" env-secret check : {env_status}") + for note in self.env_notes: + lines.append(f" {note}") for finding in self.redactable_findings: lines.append(f" {finding}") if self.deployed: @@ -280,6 +307,9 @@ def _alert(result: RefreshResult, alert_fn: AlertFn) -> None: def refresh( *, dry_run: bool = False, + # Defaults to True — fail closed. A run that did not say it had a human attached is + # assumed not to, so the stricter gate applies unless the operator opts out. + automated: bool = True, runner: Runner = _default_runner, alert_fn: AlertFn = dispatch, now: datetime | None = None, @@ -305,7 +335,7 @@ def refresh( from quantlab.glassbox.verify_dist import verify_dist started = now if now is not None else datetime.now(UTC) - result = RefreshResult(started_at=started, dry_run=dry_run) + result = RefreshResult(started_at=started, dry_run=dry_run, automated=automated) # Provenance first, so it is on the report even if the chain aborts at step one. # Report-only: a dirty tree never blocks a publish. @@ -340,6 +370,9 @@ def abort(step: str, reason: str) -> RefreshResult: report = snapshot.report result.snapshot_report_text = report.render() + result.snapshot_env_checked = bool(getattr(report, "env_checked", True)) + if getattr(report, "env_note", None): + result.env_notes.append(f"snapshot: {report.env_note}") result.redaction_count = report.redaction_count result.forbidden_matches = [f.pattern for f in report.failures] snap.ok = True @@ -378,6 +411,11 @@ def abort(step: str, reason: str) -> RefreshResult: gate.detail = str(exc) return abort(STEP_VERIFY, gate.detail) result.verify_report_text = verified.render() + dist_report = getattr(verified, "report", None) + result.dist_env_checked = bool(getattr(dist_report, "env_checked", True)) + dist_env_note = getattr(dist_report, "env_note", None) + if dist_env_note: + result.env_notes.append(f"verify-dist: {dist_env_note}") result.redactable_findings = list(verified.redactable_findings) result.forbidden_matches = sorted( set(result.forbidden_matches) | {f.pattern for f in verified.report.failures} @@ -392,6 +430,29 @@ def abort(step: str, reason: str) -> RefreshResult: f"{len(result.redactable_findings)} redactable finding(s)") # -- doctrine gate: clean, or a human looks ---------------------------- + # + # AN UNCHECKED SECRET GATE IS TREATED EXACTLY AS A REDACTION: hold the bytes, raise a + # WARNING, let a human look. Consequence of Defect #2 (2026-08-15): the `.env` + # fallback in `verify_dist` was CWD-relative, and a missing `.env` is not an error + # there — `load_env_secret_prefixes` returns an empty prefix set with a note and the + # gate still reports PASS. Under the scheduler that combination would have published + # bytes whose env-secret half had searched for nothing, while the chain report said + # PASS. The path bug is fixed; this closes the class, because "the check silently did + # not run" must never be indistinguishable from "the check ran and found nothing". + # + # AUTOMATED ONLY. Interactive runs and `--dry-run` keep the note and proceed: there a + # human is reading the report, the note is visible in it, and nothing publishes + # without them. The distinction is not convenience — it is that the abort exists to + # substitute for a reader who is absent, so it fires exactly when the reader is. + if result.automated and not dry_run and not result.env_checked: + notes = "; ".join(result.env_notes) or "no note recorded" + return abort( + STEP_DEPLOY, + "env-secret check did NOT run " + f"({notes}); an automated deploy requires the secret search to have actually " + "executed, so these bytes are held for human pre-review exactly as a " + "redaction would hold them", + ) if result.redaction_count > 0: return abort( STEP_DEPLOY, diff --git a/src/quantlab/glassbox/serve.py b/src/quantlab/glassbox/serve.py index 4dcdfd8..9c56ce1 100644 --- a/src/quantlab/glassbox/serve.py +++ b/src/quantlab/glassbox/serve.py @@ -21,13 +21,19 @@ from pathlib import Path from typing import Any +from quantlab.constants import PROJECT_ROOT + # Hardcoded. See the module docstring before considering a change. HOST = "127.0.0.1" DEFAULT_PORT = 8600 # Where `quantlab glassbox snapshot` writes by default. Inside the frontend's # `public/` tree so `vite build` copies it into `dist/` untouched. -DEFAULT_SNAPSHOT_DIR = Path("frontend") / "public" / "snapshot" +# Anchored to the repo root — see `snapshot.DEFAULT_REPORT_DIR`. This one is only +# reachable through an interactive `glassbox serve` / `glassbox snapshot --out`, so it +# never ran unattended, but it is the same defect and is fixed with its siblings rather +# than left as the one that got away. +DEFAULT_SNAPSHOT_DIR = PROJECT_ROOT / "frontend" / "public" / "snapshot" def serve( diff --git a/src/quantlab/glassbox/snapshot.py b/src/quantlab/glassbox/snapshot.py index 6cb0815..30414f2 100644 --- a/src/quantlab/glassbox/snapshot.py +++ b/src/quantlab/glassbox/snapshot.py @@ -32,6 +32,7 @@ from pydantic import BaseModel from quantlab.config import APPROVED_STRATEGIES +from quantlab.constants import PROJECT_ROOT from quantlab.glassbox import readers from quantlab.glassbox.app import create_app from quantlab.glassbox.paths import GlassboxPaths @@ -64,7 +65,19 @@ def depth_for(path: str) -> int: # "authorization_header" — so `verify-dist` matched the gate's own vocabulary and failed # the build on its own report. Publishing a document that describes your secret-detection # rules is also just poor practice. -DEFAULT_REPORT_DIR = Path("reports") / "glassbox" +# +# ANCHORED TO THE REPO ROOT, NOT THE CWD. This was a bare relative `Path("reports")` +# until 2026-08-15, which made it the only path in the snapshot step not derived from +# `PROJECT_ROOT`. Every read goes through `GlassboxPaths`, so reads were CWD-independent +# and the defect stayed invisible under manual runs from the repo root. The first +# UNATTENDED run found it: `schtasks` supplies no working directory, so the process ran +# in `C:\Windows\System32`, `mkdir(parents=True)` tried to create `reports\glassbox` +# there, and the recursion into the missing parent raised +# `PermissionError: [WinError 5] Access is denied: 'reports'` — an error naming the +# operator's reports tree while in fact describing a directory in System32 that had +# never existed. The snapshot files had already been written (their out_dir IS +# absolute), so the chain aborted with published bytes on disk and no deploy. +DEFAULT_REPORT_DIR = PROJECT_ROOT / "reports" / "glassbox" # Query parameters that do not participate in snapshot addressing (see module docstring). _KEY_EXCLUDED_PARAMS = frozenset({"limit"}) diff --git a/src/quantlab/glassbox/verify_dist.py b/src/quantlab/glassbox/verify_dist.py index 9757423..5be3e96 100644 --- a/src/quantlab/glassbox/verify_dist.py +++ b/src/quantlab/glassbox/verify_dist.py @@ -22,6 +22,7 @@ from pydantic import BaseModel +from quantlab.constants import PROJECT_ROOT from quantlab.glassbox.completeness import ( DEFAULT_MAX_AGE_DAYS, ContentReport, @@ -34,6 +35,17 @@ scan_forbidden, ) +# Where the env-secret half of the gate reads its needles from when the caller names no +# path. ANCHORED TO THE REPO ROOT — see `snapshot.DEFAULT_REPORT_DIR` for the incident. +# This one would have failed WORSE than that crash: a missing `.env` is not an error +# here, `load_env_secret_prefixes` returns an empty prefix set with a note and the gate +# PASSES having searched for no secrets at all. Under the scheduler (no working +# directory, so CWD is `C:\Windows\System32`) a relative `.env` never resolves, so this +# half of the published-bytes gate would have silently become a no-op the moment the +# snapshot step stopped aborting ahead of it. A gate that quietly checks nothing is worse +# than one that fails loudly. +DEFAULT_ENV_PATH = PROJECT_ROOT / ".env" + # Extensions worth scanning as text. Binary assets (png/ico/woff2) cannot carry a # greppable secret in any form this gate could detect, and decoding them as text would # produce noise; they are counted as skipped so the report is explicit about coverage. @@ -133,7 +145,7 @@ def verify_dist( if not dist_dir.exists(): raise FileNotFoundError(f"no such directory: {dist_dir}") - resolved_env = env_path if env_path is not None else Path(".env") + resolved_env = env_path if env_path is not None else DEFAULT_ENV_PATH env = load_env_secret_prefixes(resolved_env) forbidden_counts: dict[str, int] = {} @@ -219,4 +231,7 @@ def verify_dist( ) -__all__ = ["verify_dist", "DistVerifyResult", "TEXT_SUFFIXES", "BINARY_SUFFIXES"] +__all__ = [ + "verify_dist", "DistVerifyResult", "TEXT_SUFFIXES", "BINARY_SUFFIXES", + "DEFAULT_ENV_PATH", +] diff --git a/src/quantlab/improve/__init__.py b/src/quantlab/improve/__init__.py new file mode 100644 index 0000000..f715cf6 --- /dev/null +++ b/src/quantlab/improve/__init__.py @@ -0,0 +1,18 @@ +"""The AI improvement pipeline: propose -> firewall -> implement -> human merge. + +Three modules, one rule each: + +* :mod:`quantlab.improve.sources` — what the analysis may READ (an allowlist of + artifacts this system published about itself, plus the dated decision log). +* :mod:`quantlab.improve.firewall` — what a proposal may CHANGE (a denylist of frozen + paths and change classes, enforced structurally at both propose and implement time). +* :mod:`quantlab.improve.propose` / :mod:`quantlab.improve.implement` — the two halves, + kept separate so that analysis can run freely while anything that touches the tree + stays on a branch behind a human merge gate. + +Nothing here merges. That is checked by a test, not asserted by a comment. +""" + +from __future__ import annotations + +__all__ = ["firewall", "implement", "propose", "sources"] diff --git a/src/quantlab/improve/firewall.py b/src/quantlab/improve/firewall.py new file mode 100644 index 0000000..7e012cd --- /dev/null +++ b/src/quantlab/improve/firewall.py @@ -0,0 +1,321 @@ +"""The firewall: what an automated proposal may never touch. + +THIS IS STRUCTURAL, NOT ADVISORY. The distinction is the whole point. A prompt that +says "please do not tune the strategy parameters" is a request, and a request is +satisfied or not depending on how a model reads it that day. This module is a gate: +:func:`check` returns a refusal, ``propose`` will not write the file, and ``implement`` +re-runs the same check against the ACTUAL diff before it will gate or push. A proposal +that touches a forbidden path cannot become a commit no matter how good the argument +for it is, because nothing in the pipeline has a code path that emits one. + +WHY THESE THINGS SPECIFICALLY. The project's primary defense against overfitting is the +iron rule (2026-07-06): every strategy parameter is taken directly from the source +literature and is NEVER chosen or adjusted to improve a backtest metric. An automated +improvement loop is precisely the mechanism that would erode that rule fastest — it can +read the performance data, notice that a different lookback would have scored better, +and write a persuasive proposal saying so. That proposal would not be wrong on its own +terms. It would just be overfitting with a good bibliography. So the loop is not +permitted to form the thought in a way that reaches the repository. + +The same reasoning covers the other four classes. Risk limits and thresholds are +DECISIONS with a dated ruling behind them, not tunables — a system that can widen its +own alerting threshold in response to an alert has removed the alarm, not the fault +(2026-08-10, "Residual thresholding: fix the instrument, not the threshold"). Schedule +cadences determine what the paper record even means, because a changed mark interval +silently redefines every divergence figure computed against it. Sanitizer patterns are +the secret-leak gate; a loop that can widen them can publish a key. Broker logic is the +order path, which has been frozen under human review since the first paper account went +live. + +WHAT IS ALLOWED. Everything else — reporting, the Glass Box service, the frontend, +scheduling *plumbing* (as opposed to cadence), CLI ergonomics, tests, docs, CI. The loop +is meant to improve the machine around the strategy, never the strategy. + +STRATEGY-PERFORMANCE DATA MAY INFORM INFRASTRUCTURE PROPOSALS ONLY. Reading the weekly +divergence figures to notice that the weekly is silent on a clean week is legitimate and +is exactly the kind of observation this pipeline exists to produce. Reading the same +figures to notice that `trend` would have done better with a 12-month SMA is the +forbidden move. The rule is enforced on the PROPOSAL'S EFFECT, not on what it read: +evidence is unrestricted within the allowed source set, affected paths are not. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from pathlib import Path + +from quantlab.constants import PROJECT_ROOT + +# Cited verbatim in every refusal, so the refusal explains itself without the reader +# needing to already know the project's history. +IRON_RULE = ( + "IRON RULE (docs/decisions.md, 2026-07-06 'Literature-fixed parameters and the " + "\"iron rule\"'): every strategy parameter is taken directly from the source " + "literature and is never chosen or adjusted to improve a backtest metric. " + "Risk limits, alerting thresholds, schedule cadences, sanitizer patterns and the " + "broker order path are dated DECISIONS under human review, not tunables. An " + "automated proposal may improve the machine around the strategy; it may never " + "adjust the strategy, the limits that constrain it, the instruments that measure " + "it, or the gate that keeps secrets out of the published bytes." +) + + +@dataclass(frozen=True) +class ForbiddenPath: + """A repo-relative path prefix no automated change may touch.""" + + prefix: str + reason: str + + def matches(self, rel_posix: str) -> bool: + if self.prefix.endswith("/"): + return rel_posix.startswith(self.prefix) + return rel_posix == self.prefix + + +# Directories end in "/", files do not. Kept explicit rather than globbed so that reading +# this list tells you exactly what is frozen without evaluating a pattern language. +FORBIDDEN_PATHS: tuple[ForbiddenPath, ...] = ( + ForbiddenPath( + "src/quantlab/backtest/strategies/", + "strategy definitions and their literature-fixed parameters", + ), + ForbiddenPath( + "src/quantlab/broker/", + "the broker order path, frozen under human review since the first paper account", + ), + ForbiddenPath( + "src/quantlab/risk/", + "the risk engine: limits, kill-switch, and halt state", + ), + ForbiddenPath("config/risk.yaml", "equity risk limit values"), + ForbiddenPath("config/crypto_risk.yaml", "crypto risk limit values"), + ForbiddenPath( + "src/quantlab/glassbox/sanitize.py", + "sanitizer patterns — the secret-leak gate over published bytes", + ), + ForbiddenPath( + "src/quantlab/scheduling/tasks.py", + "schedule cadences, which define what the paper mark interval means", + ), +) + + +@dataclass(frozen=True) +class ForbiddenClass: + """A kind of change that is forbidden regardless of which file it lands in. + + Path rules alone are not enough: a threshold can be widened from a constant in a + reporting module, and a cadence can be changed from a CLI default. These catch the + INTENT where it is stated, in the proposal's own prose. + """ + + name: str + reason: str + # Both must appear for the class to fire — a target being discussed is not a + # proposal to mutate it, and a mutation verb alone is most of English. + targets: tuple[str, ...] + verbs: tuple[str, ...] = ( + "widen", "widening", "raise", "raising", "lower", "lowering", "relax", + "relaxing", "loosen", "loosening", "tune", "tuning", "retune", "adjust", + "adjusting", "increase", "increasing", "decrease", "decreasing", "bump", + "soften", "softening", "tighten", "tightening", "optimi", "recalibrat", + "change", "changing", "set", "reset", "override", "disable", "remove", + ) + + +FORBIDDEN_CLASSES: tuple[ForbiddenClass, ...] = ( + ForbiddenClass( + "strategy_parameters", + "a literature-fixed parameter may never be chosen or adjusted to improve a metric", + targets=( + "lookback", "sma", "moving average", "strategy parameter", "vol target", + "volatility target", "rebalance band", "momentum window", "signal lag", + ), + ), + ForbiddenClass( + "risk_limits", + "risk limits are dated decisions with a ruling behind them, not tunables", + targets=( + "risk limit", "max drawdown", "drawdown limit", "kill switch", "kill-switch", + "halt threshold", "position limit", "exposure limit", + ), + ), + ForbiddenClass( + "threshold_constants", + "widening the alarm in response to the alarm removes the alarm, not the fault", + targets=( + "threshold", "diverging threshold", "divergence threshold", "bps limit", + "alert threshold", "tolerance", + ), + ), + ForbiddenClass( + "schedule_cadence", + "a changed mark interval silently redefines every divergence figure", + targets=( + "cadence", "schedule time", "run time", "cron", "trigger time", + "how often", "frequency of the run", "mark interval", "submit cutoff", + ), + ), + ForbiddenClass( + "sanitizer_patterns", + "the sanitizer is the secret-leak gate; widening it can publish a key", + targets=( + "sanitiz", "forbidden pattern", "redaction pattern", "secret pattern", + "allowlist of secrets", "env_secret", + ), + ), + ForbiddenClass( + "broker_logic", + "the order path is frozen under human review", + targets=( + "broker logic", "order submission", "order path", "order type", + "submit logic", "fill logic", + ), + ), +) + + +@dataclass(frozen=True) +class Refusal: + """One reason a proposal was refused.""" + + kind: str # "path" | "class" + identifier: str # the offending path, or the class name + reason: str + evidence: str = "" # the phrase or path that triggered it + + +@dataclass +class FirewallVerdict: + allowed: bool + refusals: list[Refusal] = field(default_factory=list) + + def render(self) -> str: + """The refusal text. Always cites the iron rule — that is the whole point.""" + if self.allowed: + return "FIREWALL PASS — no forbidden path or change class touched." + lines = [ + "=" * 72, + "FIREWALL REFUSAL — this proposal will not be written.", + "=" * 72, + "", + ] + for r in self.refusals: + if r.kind == "path": + lines.append(f" FORBIDDEN PATH {r.identifier}") + else: + lines.append(f" FORBIDDEN CLASS {r.identifier}") + lines.append(f" {r.reason}") + if r.evidence: + lines.append(f" triggered by: {r.evidence}") + lines.append("") + lines += [ + IRON_RULE, + "", + "This refusal is structural. There is no flag that overrides it and no code " + "path in `propose` or `implement` that emits a proposal touching these. If " + "the change is genuinely warranted, it is a HUMAN decision: take it to the " + "Quant Lead, and if it is approved it is recorded as a dated ruling in " + "docs/decisions.md and applied by hand.", + ] + return "\n".join(lines) + + +def _normalise(path: str | Path) -> str: + """Repo-relative POSIX form, so matching is stable across OS and absolute inputs.""" + p = Path(path) + if p.is_absolute(): + try: + p = p.relative_to(PROJECT_ROOT) + except ValueError: + return p.as_posix() + return p.as_posix().lstrip("./") + + +def check_paths(paths: list[str] | tuple[str, ...]) -> list[Refusal]: + """Refusals for any affected path that lands inside a frozen area.""" + out: list[Refusal] = [] + for raw in paths: + rel = _normalise(raw) + for forbidden in FORBIDDEN_PATHS: + if forbidden.matches(rel): + out.append(Refusal("path", rel, forbidden.reason, evidence=rel)) + break + return out + + +def _term_pattern(term: str) -> re.Pattern[str]: + """Word-boundary matcher for one target or verb. + + Naive substring matching is wrong in both directions and was a real defect: `change` + matched inside "un**change**d", so the sentence "the threshold value itself is + unchanged" — an explicit statement that nothing is being tuned — was refused as a + proposal to tune it. A firewall that fires on its own disclaimers trains its operator + to route around it, which is the failure mode the sanitizer's `apca_api_header` + pattern was already narrowed to avoid (2026-07-26). + + Leading `\\b` always; trailing `\\b` too for short alphabetic terms, where a prefix + match is nearly always a different word — `set` in "settled", `sma` in "smart", + `cron` in "chronic". Longer terms keep the open end deliberately, so `optimi` covers + optimise/optimize/optimisation and `raise` covers raises/raised. + """ + escaped = re.escape(term) + if len(term) <= 4 and term.isalpha(): + return re.compile(rf"\b{escaped}\b", re.IGNORECASE) + return re.compile(rf"\b{escaped}", re.IGNORECASE) + + +def _first_match(terms: tuple[str, ...], text: str) -> str | None: + return next((t for t in terms if _term_pattern(t).search(text)), None) + + +def check_text(text: str) -> list[Refusal]: + """Refusals for a forbidden change class stated in the proposal's own prose. + + Fails CLOSED by design: a proposal that merely discusses a threshold in mutating + language is refused even when the mutation was not the point. Over-refusal costs a + human sentence of clarification; under-refusal costs the iron rule. + """ + out: list[Refusal] = [] + for cls in FORBIDDEN_CLASSES: + hit_target = _first_match(cls.targets, text) + if hit_target is None: + continue + hit_verb = _first_match(cls.verbs, text) + if hit_verb is None: + continue + # Quote the sentence that fired, so the refusal is actionable rather than cryptic. + # Both halves must co-occur IN THE SAME SENTENCE for the quote to be honest; if + # they never do, the class still fires but says so without inventing a quote. + sentence = "" + for candidate in re.split(r"(?<=[.!?])\s+|\n", text): + if _first_match(cls.targets, candidate) and _first_match(cls.verbs, candidate): + sentence = candidate.strip() + break + out.append(Refusal( + "class", cls.name, cls.reason, + evidence=f'"{sentence}"' if sentence else f"target={hit_target!r} verb={hit_verb!r}", + )) + return out + + +def check(*, affected_paths: list[str] | tuple[str, ...] = (), text: str = "") -> FirewallVerdict: + """The gate. Both halves run; a proposal must clear paths AND prose.""" + refusals = check_paths(affected_paths) + check_text(text) + return FirewallVerdict(allowed=not refusals, refusals=refusals) + + +__all__ = [ + "IRON_RULE", + "ForbiddenPath", + "ForbiddenClass", + "FORBIDDEN_PATHS", + "FORBIDDEN_CLASSES", + "Refusal", + "FirewallVerdict", + "check", + "check_paths", + "check_text", +] diff --git a/src/quantlab/improve/implement.py b/src/quantlab/improve/implement.py new file mode 100644 index 0000000..5b490dd --- /dev/null +++ b/src/quantlab/improve/implement.py @@ -0,0 +1,370 @@ +"""`quantlab implement PROP-n` — apply on a branch, prove it, push, and STOP. + +THE STOP IS THE FEATURE. This module creates ``prop/{n}``, applies the change, runs the +full gate battery, writes an implementation report into the proposal, commits, pushes, +and ends. It does not merge. It cannot merge: there is no code path here that runs +``git merge``, ``git rebase``, or a push to ``main``, and a test asserts that by reading +this file's own source. Merge is Daniel's, via pull request, after Quant Lead review. + +WHY THE HUMAN GATE IS STRUCTURAL AND NOT A CONVENTION. The pipeline's whole claim is +that an automated loop can improve this system without being trusted. A loop that can +merge its own work is trusted by construction — every safety property downstream of it +reduces to "the analysis was right", which is the one thing that cannot be guaranteed. +Keeping the last step human means the worst case of a wrong proposal is a branch nobody +merges, which costs a review and nothing else. + +DEFENCE IN DEPTH ON THE FIREWALL. `propose` already refused forbidden paths before +writing the document. This re-runs the same check against the ACTUAL diff, because the +document and the diff are different artifacts and only the second one becomes a commit. +A patch that quietly edits `config/risk.yaml` while the proposal says it edits the +frontend is caught here, at the point where it would otherwise matter. +""" + +from __future__ import annotations + +import shutil +import subprocess +from collections.abc import Sequence +from dataclasses import dataclass, field +from datetime import UTC, datetime +from pathlib import Path +from typing import Protocol + +from quantlab.constants import PROJECT_ROOT +from quantlab.improve import firewall +from quantlab.improve import propose as propose_mod + +BRANCH_PREFIX = "prop/" +# The branch this pipeline must never write to. Named once so the guard and the report +# agree, and so a reader can find every mention of it. +PROTECTED_BRANCH = "main" + +REPORT_ANCHOR = "" + + +class Runner(Protocol): + def __call__( + self, cmd: Sequence[str], cwd: Path + ) -> subprocess.CompletedProcess[str]: ... + + +def _default_runner(cmd: Sequence[str], cwd: Path) -> subprocess.CompletedProcess[str]: + """Run ``cmd`` in ``cwd``. Never uses a shell — arguments stay a list end to end.""" + return subprocess.run( + list(cmd), cwd=str(cwd), capture_output=True, text=True, shell=False, + ) + + +class NotOnBranch(RuntimeError): + """The working branch is not ``prop/{n}``. Nothing further will run.""" + + +@dataclass +class Gate: + """One verification command and what it said.""" + + name: str + cmd: tuple[str, ...] + ok: bool = False + skipped: bool = False + skip_reason: str = "" + detail: str = "" + + @property + def status(self) -> str: + if self.skipped: + return "SKIP" + return "PASS" if self.ok else "FAIL" + + +@dataclass +class ImplementResult: + number: int + branch: str + proposal_path: Path + started_at: datetime + applied: bool = False + apply_detail: str = "" + diffstat: str = "" + changed_paths: list[str] = field(default_factory=list) + gates: list[Gate] = field(default_factory=list) + firewall_text: str = "" + firewall_ok: bool = True + committed: bool = False + commit_sha: str = "" + pushed: bool = False + push_detail: str = "" + aborted: str = "" + # False while the report is being written INTO the proposal, true once the run has + # finished. The report has to be written before the commit — it is part of what gets + # committed — so at write time `committed` and `pushed` are necessarily still False. + # Rendering them as False into the durable artifact stated the opposite of the truth: + # the first dogfood run pushed successfully and left a file on the branch claiming it + # had done neither. The file version therefore reports what is KNOWN at write time and + # says plainly that the commit and push follow; the console version, rendered last, + # carries the SHA and the push result. + finalised: bool = False + + @property + def gates_ok(self) -> bool: + return all(g.ok or g.skipped for g in self.gates) + + @property + def ok(self) -> bool: + return not self.aborted and self.firewall_ok and self.gates_ok + + def render(self) -> str: + stamp = self.started_at.isoformat().replace("+00:00", "Z") + lines = [ + "## Implementation report", + "", + f"_implemented {stamp} | branch `{self.branch}` | " + f"status: **{'GATES PASSED' if self.ok else 'NEEDS ATTENTION'}**_", + "", + ] + if self.aborted: + lines += [f"**ABORTED:** {self.aborted}", ""] + + lines += ["### Diff stat", "", "```", self.diffstat.strip() or "(no changes)", "```", ""] + + lines += ["### Firewall re-check (against the actual diff)", "", "```", + self.firewall_text.strip(), "```", ""] + + lines += ["### Gates", "", "| gate | result | detail |", "|---|---|---|"] + for g in self.gates: + detail = (g.skip_reason if g.skipped else g.detail).replace("|", "\\|") + lines.append(f"| `{g.name}` | {g.status} | {detail} |") + lines.append("") + + lines += ["### Branch", "", f"- branch: `{self.branch}`"] + if self.finalised: + lines.append(f"- committed: **{self.committed}**") + if self.commit_sha: + lines.append(f"- commit: `{self.commit_sha}`") + lines.append(f"- pushed: **{self.pushed}** — {self.push_detail or 'n/a'}") + else: + lines.append( + "- commit and push: performed immediately after this report was written " + "into the proposal, since the report is part of what gets committed. The " + "resulting SHA and push result are in the run output, and the commit " + "itself is the one carrying this file." + ) + lines += [ + "", + "### Merge gate — STOPPED HERE", + "", + f"This pipeline does not merge. The change sits on `{self.branch}` and " + f"`{PROTECTED_BRANCH}` is untouched. **Daniel merges via pull request after " + "Quant Lead review.** There is no automated path to " + f"`{PROTECTED_BRANCH}` in `quantlab implement` — verified by test, not by " + "convention.", + "", + ] + return "\n".join(lines) + + +def _git(runner: Runner, root: Path, *args: str) -> subprocess.CompletedProcess[str]: + return runner(["git", *args], root) + + +def current_branch(runner: Runner, root: Path) -> str: + return _git(runner, root, "rev-parse", "--abbrev-ref", "HEAD").stdout.strip() + + +def _assert_on_prop_branch(runner: Runner, root: Path, number: int) -> str: + """The guard. Called before the commit and again before the push. + + Two independent calls on purpose: a single check at the top would be a check of what + the branch was, not of what it is at the moment something is written. + """ + branch = current_branch(runner, root) + expected = f"{BRANCH_PREFIX}{number}" + if branch != expected: + raise NotOnBranch( + f"refusing to write: expected branch {expected!r}, on {branch!r}. " + f"`implement` never commits or pushes anywhere but its own prop branch, and " + f"never to {PROTECTED_BRANCH!r}." + ) + return branch + + +def _frontend_touched(paths: Sequence[str]) -> bool: + return any(p.startswith("frontend/") for p in paths) + + +def implement( + number: int, + *, + patch: Path | None = None, + root: Path | None = None, + proposals_dir: Path | None = None, + runner: Runner | None = None, + push: bool = True, + now: datetime | None = None, +) -> ImplementResult: + """Branch, apply, gate, report, commit, push, stop.""" + run = runner if runner is not None else _default_runner + repo = root if root is not None else PROJECT_ROOT + started = now or datetime.now(UTC) + + proposal_path = propose_mod.find_proposal(number, proposals_dir) + branch = f"{BRANCH_PREFIX}{number}" + result = ImplementResult( + number=number, branch=branch, proposal_path=proposal_path, started_at=started, + ) + + def abort(reason: str) -> ImplementResult: + result.aborted = reason + return result + + # -- 1. branch --------------------------------------------------------- + # `checkout -B` is deliberate: re-running `implement` for the same proposal resets + # the branch rather than failing or stacking a second attempt on the first. + made = _git(run, repo, "checkout", "-B", branch) + if made.returncode != 0: + return abort(f"could not create branch {branch}: {made.stderr.strip()}") + + # -- 2. apply ---------------------------------------------------------- + if patch is not None: + applied = _git(run, repo, "apply", "--index", str(patch)) + if applied.returncode != 0: + return abort(f"patch did not apply: {applied.stderr.strip()}") + result.applied = True + result.apply_detail = f"applied {patch}" + else: + result.apply_detail = ( + "no --patch given; using changes already present in the working tree" + ) + # ONLY in worktree mode. `git apply --index` has already staged exactly the + # patch's files, and an `add -A` on top of it would sweep every unrelated dirty + # path in the checkout into the proposal's commit — which is precisely what a + # reviewer reading "affected files: frontend/src/content/copy.ts" would not + # expect. Worktree mode has no such boundary to draw, so it stages everything + # and the diff stat in the report shows exactly what that turned out to be; + # nothing is hidden, but `--patch` is the mode with a defensible blast radius. + _git(run, repo, "add", "-A") + + # -- 3. what actually changed ----------------------------------------- + result.diffstat = _git(run, repo, "diff", "--cached", "--stat").stdout.strip() + names = _git(run, repo, "diff", "--cached", "--name-only").stdout.strip() + result.changed_paths = [p for p in names.splitlines() if p] + + if not result.changed_paths: + return abort("nothing to implement: the staged diff is empty") + + # -- 4. firewall, against the DIFF this time -------------------------- + verdict = firewall.check(affected_paths=result.changed_paths) + result.firewall_ok = verdict.allowed + result.firewall_text = verdict.render() + if not verdict.allowed: + # Unstage so a refused attempt does not leave a staged forbidden change behind. + _git(run, repo, "reset") + return abort( + "the actual diff touches a firewall path; see the refusal above. " + "Nothing was committed." + ) + + # -- 5. gates ---------------------------------------------------------- + result.gates = _run_gates(run, repo, result.changed_paths) + + # -- 6. report into the proposal -------------------------------------- + write_report(proposal_path, result) + _git(run, repo, "add", str(proposal_path)) + + # -- 7. commit --------------------------------------------------------- + _assert_on_prop_branch(run, repo, number) + title = proposal_path.stem + message = ( + f"PROP-{number}: {title}\n\n" + f"Implemented by `quantlab implement`. Gates: " + f"{'all passed' if result.gates_ok else 'SEE REPORT — not all passed'}.\n" + f"Merge is human-only: Daniel merges via PR after Quant Lead review." + ) + committed = _git(run, repo, "commit", "-m", message) + if committed.returncode != 0: + return abort(f"commit failed: {committed.stderr.strip() or committed.stdout.strip()}") + result.committed = True + result.commit_sha = _git(run, repo, "rev-parse", "--short", "HEAD").stdout.strip() + + # -- 8. push, then STOP ------------------------------------------------ + if push: + _assert_on_prop_branch(run, repo, number) + pushed = _git(run, repo, "push", "--set-upstream", "origin", branch) + result.pushed = pushed.returncode == 0 + if result.pushed: + result.push_detail = f"pushed to origin/{branch}" + else: + noise = (pushed.stderr or pushed.stdout).strip().splitlines() + result.push_detail = f"push failed: {noise[-1] if noise else 'unknown error'}" + else: + result.push_detail = "push suppressed (--no-push)" + + # Only the console rendering may claim the commit and push, and only now that both + # have actually happened. There is deliberately nothing after this point. + result.finalised = True + return result + + +def _run_gates(run: Runner, repo: Path, changed: Sequence[str]) -> list[Gate]: + """ruff, mypy, pytest, the frontend suite, and verify-dist when the site is touched.""" + gates: list[Gate] = [] + + def add(name: str, cmd: tuple[str, ...], *, cwd: Path | None = None, + skip_reason: str = "") -> None: + gate = Gate(name=name, cmd=cmd) + if skip_reason: + gate.skipped, gate.skip_reason = True, skip_reason + gates.append(gate) + return + proc = run(list(cmd), cwd or repo) + gate.ok = proc.returncode == 0 + tail = (proc.stdout or proc.stderr).strip().splitlines() + gate.detail = tail[-1][:200] if tail else f"exit {proc.returncode}" + gates.append(gate) + + add("ruff", ("uv", "run", "ruff", "check", ".")) + add("mypy", ("uv", "run", "mypy", "src/quantlab")) + add("pytest", ("uv", "run", "pytest", "-q")) + + frontend = repo / "frontend" + npm = shutil.which("npm") + if not _frontend_touched(changed): + add("frontend", (), skip_reason="no frontend/ path in the diff") + elif npm is None: + add("frontend", (), skip_reason="npm not on PATH") + else: + add("frontend", (npm, "run", "test"), cwd=frontend) + add("frontend-lint", (npm, "run", "lint"), cwd=frontend) + + # verify-dist only means something against a built site. + if _frontend_touched(changed): + if (frontend / "dist").is_dir(): + add("verify-dist", ("uv", "run", "quantlab", "glassbox", "verify-dist")) + else: + add("verify-dist", (), skip_reason="frontend/dist not built in this checkout") + else: + add("verify-dist", (), skip_reason="site not touched") + + return gates + + +def write_report(proposal_path: Path, result: ImplementResult) -> None: + """Append the report at the anchor, replacing any report from an earlier attempt.""" + text = proposal_path.read_text(encoding="utf-8") + head = text.split(REPORT_ANCHOR)[0] if REPORT_ANCHOR in text else text.rstrip() + "\n\n---\n\n" + body = head + REPORT_ANCHOR + "\n\n" + result.render() + proposal_path.write_text(body.rstrip() + "\n", encoding="utf-8") + + +__all__ = [ + "BRANCH_PREFIX", + "PROTECTED_BRANCH", + "REPORT_ANCHOR", + "Gate", + "ImplementResult", + "NotOnBranch", + "Runner", + "current_branch", + "implement", + "write_report", +] diff --git a/src/quantlab/improve/propose.py b/src/quantlab/improve/propose.py new file mode 100644 index 0000000..f1a60b6 --- /dev/null +++ b/src/quantlab/improve/propose.py @@ -0,0 +1,216 @@ +"""`quantlab propose` — write a proposal, never a line of code. + +The command reads the artifacts listed in :mod:`quantlab.improve.sources`, runs the +candidate through :mod:`quantlab.improve.firewall`, and — only if the firewall passes — +writes ``docs/proposals/PROP-{n}-{slug}.md``. + +IT NEVER EDITS CODE. Not as a matter of discipline but of construction: the only write +this module performs is the proposal file itself, and the only directory it can write +into is ``docs/proposals``. Applying a change is `implement`'s job, on a branch, behind +a human merge gate. + +The separation matters because the two halves have genuinely different risk. Writing a +document is safe and can be wrong without cost. Touching the tree is neither. Keeping +them in separate commands means the analysis can run as often as you like — including +unattended — without any path by which a bad observation becomes a bad commit. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from datetime import UTC, datetime +from pathlib import Path + +from quantlab.constants import PROJECT_ROOT +from quantlab.improve import firewall, sources + +PROPOSALS_DIR = PROJECT_ROOT / "docs" / "proposals" + +# A proposal's blast radius, stated by the author and checked by the reviewer. Ordered +# least to most consequential; `implement` prints it back before it gates. +RISK_CLASSES = ("cosmetic", "content", "infrastructure", "operational") + +_SLUG_STRIP = re.compile(r"[^a-z0-9]+") + + +class ProposalRefused(RuntimeError): + """The firewall refused. Carries the rendered refusal for the caller to print.""" + + def __init__(self, verdict: firewall.FirewallVerdict) -> None: + super().__init__("proposal refused by the firewall") + self.verdict = verdict + + +def slugify(title: str) -> str: + return _SLUG_STRIP.sub("-", title.lower()).strip("-")[:60] + + +def next_number(proposals_dir: Path | None = None) -> int: + """One higher than the highest PROP- on disk. Numbers are never reused.""" + directory = proposals_dir if proposals_dir is not None else PROPOSALS_DIR + if not directory.exists(): + return 1 + highest = 0 + for path in directory.glob("PROP-*.md"): + match = re.match(r"PROP-(\d+)", path.name) + if match: + highest = max(highest, int(match.group(1))) + return highest + 1 + + +@dataclass +class Proposal: + """Everything a proposal must state. No field is optional by accident.""" + + title: str + observation: str + change: str + affected_paths: list[str] + risk_class: str + test_plan: str + evidence: list[str] = field(default_factory=list) + number: int = 0 + slug: str = "" + + def __post_init__(self) -> None: + if not self.slug: + self.slug = slugify(self.title) + if self.risk_class not in RISK_CLASSES: + raise ValueError( + f"risk class {self.risk_class!r} is not one of {', '.join(RISK_CLASSES)}" + ) + + @property + def filename(self) -> str: + return f"PROP-{self.number}-{self.slug}.md" + + @property + def firewall_text(self) -> str: + """Everything the class check reads. Deliberately includes the title and test + plan — an intent stated only in the test plan is still the intent.""" + return "\n".join([self.title, self.observation, self.change, self.test_plan]) + + +def render(proposal: Proposal, *, generated_at: datetime | None = None) -> str: + """The proposal document. Stable shape so `implement` can append to it by anchor.""" + stamp = (generated_at or datetime.now(UTC)).isoformat().replace("+00:00", "Z") + lines = [ + f"# PROP-{proposal.number} — {proposal.title}", + "", + f"_proposed {stamp} | risk class: **{proposal.risk_class}** | " + f"status: **AWAITING IMPLEMENTATION**_", + "", + "## Observation", + "", + proposal.observation.strip(), + "", + "### Evidence", + "", + ] + if proposal.evidence: + lines += [f"- `{e}`" for e in proposal.evidence] + else: + lines.append("- _(none cited)_") + lines += [ + "", + "## Proposed change", + "", + proposal.change.strip(), + "", + "## Affected files", + "", + ] + lines += [f"- `{p}`" for p in proposal.affected_paths] or ["- _(none)_"] + lines += [ + "", + "## Risk class", + "", + f"**{proposal.risk_class}**", + "", + "## Test plan", + "", + proposal.test_plan.strip(), + "", + "## Firewall", + "", + "```", + firewall.check( + affected_paths=proposal.affected_paths, text=proposal.firewall_text + ).render(), + "```", + "", + "## Merge gate", + "", + "`implement` stops after pushing the branch. **Merge is human-only:** Daniel " + "merges via pull request after Quant Lead review. No automated path to `main` " + "exists in this pipeline.", + "", + "---", + "", + "", + "", + ] + return "\n".join(lines) + + +def write_proposal( + proposal: Proposal, + *, + proposals_dir: Path | None = None, + generated_at: datetime | None = None, +) -> Path: + """Gate, then write. Raises :class:`ProposalRefused` without writing anything. + + The firewall runs BEFORE the directory is created, so a refused proposal leaves no + trace on disk at all — the same "nothing is written unless the gate passes" posture + the snapshot writer takes. + """ + verdict = firewall.check( + affected_paths=proposal.affected_paths, text=proposal.firewall_text + ) + if not verdict.allowed: + raise ProposalRefused(verdict) + + for path in proposal.evidence: + sources.assert_allowed(path) + + directory = proposals_dir if proposals_dir is not None else PROPOSALS_DIR + if proposal.number == 0: + proposal.number = next_number(directory) + directory.mkdir(parents=True, exist_ok=True) + out = directory / proposal.filename + out.write_text(render(proposal, generated_at=generated_at) + "\n", encoding="utf-8") + return out + + +def find_proposal(number: int, proposals_dir: Path | None = None) -> Path: + """The file for PROP-{number}, or a clear error naming what exists.""" + directory = proposals_dir if proposals_dir is not None else PROPOSALS_DIR + matches = sorted(directory.glob(f"PROP-{number}-*.md")) if directory.exists() else [] + if not matches: + available = ( + ", ".join(sorted(p.name for p in directory.glob("PROP-*.md"))) + if directory.exists() else "(no proposals directory)" + ) + raise FileNotFoundError( + f"no proposal numbered {number}. Available: {available or '(none)'}" + ) + if len(matches) > 1: + raise RuntimeError( + f"PROP-{number} is ambiguous: {', '.join(p.name for p in matches)}" + ) + return matches[0] + + +__all__ = [ + "PROPOSALS_DIR", + "RISK_CLASSES", + "Proposal", + "ProposalRefused", + "slugify", + "next_number", + "render", + "write_proposal", + "find_proposal", +] diff --git a/src/quantlab/improve/sources.py b/src/quantlab/improve/sources.py new file mode 100644 index 0000000..9f8674d --- /dev/null +++ b/src/quantlab/improve/sources.py @@ -0,0 +1,126 @@ +"""The only things `propose` is allowed to read. + +`propose` is an ANALYSIS command. It reads artifacts this system produced about itself +and writes a document; it never reads source code and never edits anything. The allowlist +is here, in one place, for the same reason `GlassboxPaths` exists: a read-only surface +that is auditable at a glance is one you can actually reason about. + +WHY AN ALLOWLIST AND NOT A DENYLIST. The forbidden-path firewall governs what a proposal +may CHANGE. This governs what the analysis may SEE, and the two want opposite defaults. +Change is denied by exception because most of the tree is safe to edit. Reading is +allowed by exception because the point of the exercise is that observations trace to +published artifacts — a proposal justified by something in the source tree is a proposal +justified by reading the implementation, which is how you end up "fixing" a measurement +to agree with the code rather than the other way round. + +Every source here is an artifact the system emitted about its own behaviour, or a dated +human ruling. That is the entire evidence base. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path + +from quantlab.constants import PROJECT_ROOT + + +@dataclass(frozen=True) +class Source: + """One allowed evidence location.""" + + name: str + path: Path + what: str + # A directory of artifacts, or a single file. + is_dir: bool = True + + @property + def rel(self) -> str: + try: + return self.path.relative_to(PROJECT_ROOT).as_posix() + except ValueError: + return self.path.as_posix() + + def exists(self) -> bool: + return self.path.exists() + + def inventory(self) -> list[Path]: + """Concrete files available under this source, newest last. Never recurses into + anything unexpected — a directory source lists its own files only.""" + if not self.exists(): + return [] + if not self.is_dir: + return [self.path] + return sorted(p for p in self.path.iterdir() if p.is_file()) + + +def allowed_sources() -> tuple[Source, ...]: + """The complete read set, resolved against the repo root (never the CWD).""" + r = PROJECT_ROOT + return ( + Source("weekly", r / "reports" / "weekly", "weekly review markdown + json"), + Source("digests", r / "reports" / "digests", "daily paper digests"), + Source("runs", r / "reports" / "paper", "per-run paper reports"), + Source("alerts", r / "reports" / "alerts" / "alerts.jsonl", + "the alert stream", is_dir=False), + Source("ci", r / ".github" / "workflows", "CI workflow definition + status"), + Source("site", r / "reports" / "glassbox", + "Glass Box / Lighthouse / site artifacts"), + Source("decisions", r / "docs" / "decisions.md", + "the dated decision log", is_dir=False), + Source("proposals", r / "docs" / "proposals", + "previously written proposals (so numbering and duplication are visible)"), + ) + + +class SourceViolation(RuntimeError): + """An evidence path was cited that is outside the allowed read set.""" + + +def assert_allowed(path: str | Path) -> Path: + """Resolve an evidence path and prove it lies inside an allowed source. + + Raises :class:`SourceViolation` otherwise. Called on every `--evidence` argument, so + a proposal cannot cite the source tree as its justification. + """ + p = Path(path) + resolved = (p if p.is_absolute() else PROJECT_ROOT / p).resolve() + for source in allowed_sources(): + root = source.path.resolve() + if resolved == root or (source.is_dir and resolved.is_relative_to(root)): + return resolved + permitted = ", ".join(s.rel for s in allowed_sources()) + raise SourceViolation( + f"evidence path is outside the allowed read set: {path}\n" + f" `propose` may read ONLY: {permitted}\n" + f" Source code is deliberately not readable here — an observation must trace " + f"to an artifact this system published about itself, not to the implementation." + ) + + +def render_inventory() -> str: + """What the analysis actually had available, including what was missing. + + Absent sources are reported, not skipped. A proposal written in a week where the + Lighthouse artifacts were never produced should say so rather than quietly narrow + its evidence base. + """ + lines = ["EVIDENCE SOURCES READ", "-" * 72] + for source in allowed_sources(): + if not source.exists(): + lines.append(f" {source.name:<10} {source.rel:<34} ABSENT — not produced yet") + continue + files = source.inventory() + count = f"{len(files)} file(s)" if source.is_dir else "present" + lines.append(f" {source.name:<10} {source.rel:<34} {count} ({source.what})") + return "\n".join(lines) + + +__all__ = [ + "Source", + "SourceViolation", + "allowed_sources", + "assert_allowed", + "render_inventory", +] diff --git a/tests/test_glassbox_refresh.py b/tests/test_glassbox_refresh.py index 9f504ba..22fa162 100644 --- a/tests/test_glassbox_refresh.py +++ b/tests/test_glassbox_refresh.py @@ -87,9 +87,17 @@ def __init__(self, passed: bool = True): class FakeVerified: + # `env_checked` defaults to True here because the double stands in for a NORMAL run, + # in which the secret search did execute. It is False on the model by default, which + # is the right default for the model (absence of evidence is not a passing check) but + # the wrong one for a fixture claiming to represent a clean gate. def __init__(self, *, passed: bool = True, redactable: list[str] | None = None, - forbidden: list[ForbiddenRecord] | None = None): - self.report = SanitizationReport(passed=not forbidden, forbidden=forbidden or []) + forbidden: list[ForbiddenRecord] | None = None, + env_checked: bool = True, env_note: str | None = None): + self.report = SanitizationReport( + passed=not forbidden, forbidden=forbidden or [], + env_checked=env_checked, env_note=env_note, + ) self.content = FakeContent(passed) self.passed = passed and not forbidden self.redactable_findings = redactable or [] @@ -100,7 +108,8 @@ def render(self) -> str: return "VERIFY REPORT BODY" -def _clean_report(redactions: int = 0) -> SanitizationReport: +def _clean_report(redactions: int = 0, *, env_checked: bool = True, + env_note: str | None = None) -> SanitizationReport: records = ( [RedactionRecord(pattern="windows_user_path", location="assets/index.js", count=redactions, replacement="")] @@ -109,11 +118,13 @@ def _clean_report(redactions: int = 0) -> SanitizationReport: return SanitizationReport( passed=True, files_scanned=22, bytes_scanned=27_321, redactions=records, forbidden=[ForbiddenRecord(pattern="alpaca_account_id", count=0)], + env_checked=env_checked, env_note=env_note, ) def _run(*, runner: FakeRunner | None = None, report: SanitizationReport | None = None, verified: FakeVerified | None = None, dry_run: bool = False, + automated: bool = True, snapshot_raises: SanitizationError | None = None, alerts: list[Alert] | None = None, tmp: Path | None = None): used_runner = runner or FakeRunner() @@ -131,7 +142,8 @@ def verify_fn(dist: Path, **kwargs: object) -> FakeVerified: return verified or FakeVerified() result = refresh( - dry_run=dry_run, runner=used_runner, alert_fn=lambda a: sink.append(a) or [], + dry_run=dry_run, automated=automated, + runner=used_runner, alert_fn=lambda a: sink.append(a) or [], now=NOW, snapshot_fn=snapshot_fn, verify_fn=verify_fn, frontend_dir=tmp or Path("frontend"), dist_dir=(tmp or Path("frontend")) / "dist", ) @@ -412,3 +424,83 @@ def test_an_aborted_report_says_where_and_why() -> None: assert "aborted at 'deploy'" in rendered assert "redactions : 2" in rendered assert "SKIPPED" in rendered # the deploy step is visibly not run + + +# --------------------------------------------------------------------------- # +# The env-secret gate (2026-08-15, consequence of Defect #2) # +# --------------------------------------------------------------------------- # +# +# `load_env_secret_prefixes` treats a missing `.env` as a note, not an error: it returns +# an empty prefix set and the surrounding gate still reports PASS. Combined with the +# CWD-relative `.env` fallback that Defect #2 fixed, an unattended chain could have +# published bytes whose env-secret half searched for nothing while the report said PASS. +# The path bug is fixed; these tests close the class. + + +_NO_ENV = "no .env at .env; secret-prefix check skipped" + + +def test_automated_run_aborts_when_the_env_secret_check_did_not_run() -> None: + """The ruling: NOT CHECKED holds the bytes exactly as a redaction does.""" + result, runner, alerts, _c = _run( + verified=FakeVerified(env_checked=False, env_note=_NO_ENV), + ) + assert not result.ok + assert result.aborted_at == "deploy" + assert "env-secret check did NOT run" in (result.abort_reason or "") + assert "held for human pre-review" in (result.abort_reason or "") + # Held, not published: the deploy command never ran. + assert "deploy" not in runner.ran + assert not result.deployed + # And a human is told, at WARNING, exactly as a redaction abort does. + assert len(alerts) == 1 + assert alerts[0].level == "WARNING" + + +def test_automated_run_aborts_when_the_snapshot_half_did_not_check() -> None: + """Either half being unchecked is enough; the gate is the conjunction of both.""" + result, runner, _a, _c = _run( + report=_clean_report(env_checked=False, env_note=_NO_ENV), + ) + assert not result.ok + assert result.aborted_at == "deploy" + assert "deploy" not in runner.ran + + +def test_interactive_run_keeps_the_note_and_deploys() -> None: + """A human is reading the report, so the note is enough and nothing is held.""" + result, runner, _a, _c = _run( + automated=False, verified=FakeVerified(env_checked=False, env_note=_NO_ENV), + ) + assert result.ok, result.abort_reason + assert result.deployed + assert "deploy" in runner.ran + assert not result.env_checked + rendered = result.render() + assert "NOT CHECKED (note only" in rendered + assert _NO_ENV in rendered + + +def test_dry_run_keeps_the_note_and_does_not_abort() -> None: + """`--dry-run` publishes nothing, so the stricter gate has nothing to protect.""" + result, runner, _a, _c = _run( + dry_run=True, verified=FakeVerified(env_checked=False, env_note=_NO_ENV), + ) + assert result.ok, result.abort_reason + assert not result.deployed + assert "deploy" not in runner.ran + assert _NO_ENV in result.render() + + +def test_a_checked_env_gate_deploys_normally_and_says_so() -> None: + """The gate must not fire on the happy path, or it is just an off switch.""" + result, runner, _a, _c = _run() + assert result.ok and result.deployed + assert result.env_checked + assert "env-secret check : ran" in result.render() + + +def test_the_env_gate_is_reported_on_every_run_not_only_on_failure() -> None: + """"The check ran" should be confirmable, not inferred from the absence of a note.""" + assert "env-secret check" in _run()[0].render() + assert "env-secret check" in _run(dry_run=True)[0].render() diff --git a/tests/test_improve_firewall.py b/tests/test_improve_firewall.py new file mode 100644 index 0000000..6b7f055 --- /dev/null +++ b/tests/test_improve_firewall.py @@ -0,0 +1,172 @@ +"""The firewall must refuse, structurally, the things the iron rule freezes. + +The load-bearing test in this file is `test_widen_the_threshold_proposal_is_refused`. +The pipeline's entire safety claim is that an automated loop cannot relax the constraints +it is measured against — and the most plausible way that claim fails is not malice but +reasonableness: a loop reads a week of DIVERGING alerts, correctly observes that the +threshold produces alerts nobody acts on, and proposes widening it. That proposal would +be well-evidenced and wrong, and it is exactly what 2026-08-10 ("Residual thresholding: +fix the instrument, not the threshold") ruled against. So it is tested by name. +""" + +from __future__ import annotations + +import pytest + +from quantlab.improve import firewall +from quantlab.improve.propose import Proposal, ProposalRefused, write_proposal + + +def test_widen_the_threshold_proposal_is_refused() -> None: + """THE test. A synthetic, well-argued 'widen the threshold' proposal must not pass.""" + proposal = Proposal( + title="Widen the DIVERGING threshold from 50 bps to 120 bps", + observation=( + "Four of the last six weekly reviews fired DIVERGING on crypto_voltarget. " + "Every one was later attributed to mark-phase geometry rather than tracking " + "error, so the alerts produced no action." + ), + change=( + "Raise weekly_divergence_alert_bps from 50 to 120 so the alert fires only on " + "residuals that survive the geometry decomposition." + ), + affected_paths=["config/risk.yaml"], + risk_class="operational", + test_plan="Re-run the weekly over the last six weeks and confirm zero alerts.", + ) + + verdict = firewall.check( + affected_paths=proposal.affected_paths, text=proposal.firewall_text + ) + assert not verdict.allowed, "the firewall let a threshold-widening proposal through" + + kinds = {r.kind for r in verdict.refusals} + assert "path" in kinds, "config/risk.yaml should have been refused on its path" + assert "class" in kinds, "the stated intent should have been refused on its class" + + classes = {r.identifier for r in verdict.refusals if r.kind == "class"} + assert "threshold_constants" in classes + + +def test_widen_the_threshold_proposal_writes_nothing(tmp_path) -> None: + """A refusal must leave no file behind — the gate is before the write, not after.""" + proposal = Proposal( + title="Widen the DIVERGING threshold to 120 bps", + observation="Alerts nobody acts on.", + change="Raise the divergence threshold.", + affected_paths=["config/risk.yaml"], + risk_class="operational", + test_plan="Re-run the weekly.", + ) + with pytest.raises(ProposalRefused) as caught: + write_proposal(proposal, proposals_dir=tmp_path) + + assert list(tmp_path.iterdir()) == [], "a refused proposal left a file on disk" + assert "IRON RULE" in caught.value.verdict.render() + + +def test_refusal_text_cites_the_iron_rule() -> None: + """The refusal has to explain itself to someone who does not know the history.""" + verdict = firewall.check(affected_paths=["src/quantlab/broker/alpaca.py"]) + rendered = verdict.render() + assert not verdict.allowed + assert "IRON RULE" in rendered + assert "2026-07-06" in rendered + assert "docs/decisions.md" in rendered + assert "no flag that overrides it" in rendered + + +@pytest.mark.parametrize( + "path", + [ + "src/quantlab/backtest/strategies/trend.py", + "src/quantlab/broker/alpaca.py", + "src/quantlab/risk/limits.py", + "config/risk.yaml", + "config/crypto_risk.yaml", + "src/quantlab/glassbox/sanitize.py", + "src/quantlab/scheduling/tasks.py", + ], +) +def test_every_frozen_path_is_refused(path: str) -> None: + assert not firewall.check(affected_paths=[path]).allowed, f"{path} was not refused" + + +@pytest.mark.parametrize( + "text", + [ + "Retune the trend lookback from 10 months to 12.", + "Lower the max drawdown limit so the kill switch fires later.", + "Relax the sanitizer forbidden pattern for email_address.", + "Change the crypto run cadence to hourly.", + "Adjust the order submission logic to use marketable limits.", + "Increase the volatility target to 12%.", + ], +) +def test_forbidden_change_classes_are_refused_from_prose_alone(text: str) -> None: + """Path rules are not enough — a constant can be edited from an innocuous file.""" + verdict = firewall.check(affected_paths=["src/quantlab/reporting/weekly.py"], text=text) + assert not verdict.allowed, f"not refused: {text!r}" + assert any(r.kind == "class" for r in verdict.refusals) + + +def test_legitimate_infrastructure_proposal_passes() -> None: + """The firewall must not refuse everything, or it would just be an off switch.""" + verdict = firewall.check( + affected_paths=[ + "src/quantlab/reporting/weekly.py", + "tests/test_weekly_completion_signal.py", + ], + text=( + "The weekly emits no completion signal on an all-TRACKING week, so a clean " + "week and a week the task never ran are indistinguishable from the alert " + "stream. Emit one INFO alert recording the week ending and the four verdicts." + ), + ) + assert verdict.allowed, verdict.render() + assert "FIREWALL PASS" in verdict.render() + + +def test_discussing_a_threshold_without_proposing_to_move_it_is_allowed() -> None: + """Both halves must fire: a target alone is discussion, a verb alone is English.""" + verdict = firewall.check( + affected_paths=["src/quantlab/reporting/weekly.py"], + text=( + "The report should state which threshold the verdict was taken against, so " + "a reader does not have to infer it. The threshold value itself is unchanged." + ), + ) + assert verdict.allowed, verdict.render() + + +def test_strategy_performance_data_may_inform_an_infrastructure_proposal() -> None: + """The rule is enforced on EFFECT, not on what the analysis read. + + Reading the divergence figures to improve the reporting machinery is the intended + use. The same figures used to argue for a parameter change are refused above. + """ + verdict = firewall.check( + affected_paths=["src/quantlab/glassbox/app.py"], + text=( + "crypto_voltarget's cumulative divergence of -76 bps is dominated by mark " + "timing, but the Glass Box surfaces only the raw figure, so a reader cannot " + "see the decomposition the verdict was actually taken on. Expose the " + "residual alongside the raw divergence in the divergence endpoint." + ), + ) + assert verdict.allowed, verdict.render() + + +def test_absolute_paths_are_normalised_before_matching() -> None: + """An absolute path must not sneak past a prefix match.""" + from quantlab.constants import PROJECT_ROOT + + absolute = str(PROJECT_ROOT / "config" / "risk.yaml") + assert not firewall.check(affected_paths=[absolute]).allowed + + +def test_windows_separators_are_normalised_before_matching() -> None: + assert not firewall.check(affected_paths=[r"config\risk.yaml"]).allowed + assert not firewall.check( + affected_paths=[r"src\quantlab\broker\alpaca.py"] + ).allowed diff --git a/tests/test_improve_pipeline.py b/tests/test_improve_pipeline.py new file mode 100644 index 0000000..55f44e2 --- /dev/null +++ b/tests/test_improve_pipeline.py @@ -0,0 +1,286 @@ +"""`propose` writes documents, `implement` writes branches, and neither writes `main`. + +The two structural claims this file exists to prove: + +* `propose` NEVER edits code — enforced by construction (the only path it writes is + under its proposals directory) and by an allowlist on what it may even read. +* `implement` NEVER touches `main` — proven twice, because one proof is not enough for + a claim this load-bearing. Behaviourally, by running the real thing against a real + git repository and asserting `main`'s SHA is byte-identical afterwards. Structurally, + by reading `implement.py`'s own source and asserting no merge verb appears in it. + The behavioural test catches a bug; the source test catches a future feature. +""" + +from __future__ import annotations + +import subprocess +from datetime import UTC, datetime +from pathlib import Path + +import pytest + +from quantlab.improve import firewall +from quantlab.improve.implement import ( + BRANCH_PREFIX, + PROTECTED_BRANCH, + NotOnBranch, + current_branch, + implement, +) +from quantlab.improve.propose import ( + Proposal, + next_number, + render, + slugify, + write_proposal, +) +from quantlab.improve.sources import SourceViolation, assert_allowed + +STAMP = datetime(2026, 8, 15, 12, 0, tzinfo=UTC) + + +def _run(args: list[str], cwd: Path) -> subprocess.CompletedProcess[str]: + return subprocess.run(args, cwd=str(cwd), capture_output=True, text=True, shell=False) + + +@pytest.fixture +def repo(tmp_path: Path) -> Path: + """A real git repository with a committed file and a `main` branch.""" + root = tmp_path / "repo" + (root / "frontend" / "src").mkdir(parents=True) + (root / "docs" / "proposals").mkdir(parents=True) + (root / "frontend" / "src" / "copy.ts").write_text( + "export const COPY = 'caught itself twice'\n", encoding="utf-8" + ) + _run(["git", "init", "-b", PROTECTED_BRANCH], root) + _run(["git", "config", "user.email", "test@example.com"], root) + _run(["git", "config", "user.name", "test"], root) + _run(["git", "add", "-A"], root) + _run(["git", "commit", "-m", "initial"], root) + return root + + +def _proposal(**over: object) -> Proposal: + base: dict[str, object] = dict( + title="Emit a completion signal on an all-TRACKING week", + observation="The weekly is silent when every account tracks.", + change="Log one INFO recording the week ending and the four verdicts.", + affected_paths=["src/quantlab/reporting/weekly.py"], + risk_class="infrastructure", + test_plan="Assert one INFO is emitted on an all-TRACKING fixture week.", + ) + base.update(over) + return Proposal(**base) # type: ignore[arg-type] + + +# --------------------------------------------------------------------------- propose + + +def test_propose_writes_the_document_with_every_required_section(tmp_path: Path) -> None: + path = write_proposal(_proposal(), proposals_dir=tmp_path, generated_at=STAMP) + text = path.read_text(encoding="utf-8") + for heading in ( + "## Observation", "### Evidence", "## Proposed change", "## Affected files", + "## Risk class", "## Test plan", "## Firewall", "## Merge gate", + ): + assert heading in text, f"missing {heading}" + assert "FIREWALL PASS" in text + assert "Daniel merges via pull request" in text + + +def test_propose_writes_nothing_outside_its_proposals_directory(tmp_path: Path) -> None: + """The 'never edits code' claim, made concrete: exactly one file appears.""" + before = {p for p in tmp_path.rglob("*")} + write_proposal(_proposal(), proposals_dir=tmp_path, generated_at=STAMP) + after = {p for p in tmp_path.rglob("*")} + created = after - before + assert len(created) == 1 + assert created.pop().suffix == ".md" + + +def test_evidence_outside_the_allowed_read_set_is_rejected() -> None: + """Source code is not readable evidence — an observation traces to an artifact.""" + with pytest.raises(SourceViolation) as caught: + assert_allowed("src/quantlab/reporting/weekly.py") + assert "outside the allowed read set" in str(caught.value) + + +@pytest.mark.parametrize( + "allowed", + ["reports/weekly", "reports/paper", "reports/alerts/alerts.jsonl", "docs/decisions.md"], +) +def test_allowed_evidence_paths_are_accepted(allowed: str) -> None: + assert assert_allowed(allowed).exists() + + +def test_numbers_are_never_reused(tmp_path: Path) -> None: + assert next_number(tmp_path) == 1 + (tmp_path / "PROP-1-a.md").write_text("x", encoding="utf-8") + (tmp_path / "PROP-7-b.md").write_text("x", encoding="utf-8") + assert next_number(tmp_path) == 8 + + +def test_slugify_is_filesystem_safe() -> None: + assert slugify("Story page: 'twice' -> three!") == "story-page-twice-three" + + +def test_render_is_deterministic() -> None: + p = _proposal() + p.number = 3 + assert render(p, generated_at=STAMP) == render(p, generated_at=STAMP) + + +# ------------------------------------------------------------------------- implement + + +def test_implement_never_touches_main(repo: Path) -> None: + """The behavioural proof. `main` must be byte-identical before and after.""" + proposals = repo / "docs" / "proposals" + proposal = _proposal( + title="Story page says twice", + affected_paths=["frontend/src/copy.ts"], + risk_class="content", + ) + write_proposal(proposal, proposals_dir=proposals, generated_at=STAMP) + + main_before = _run(["git", "rev-parse", PROTECTED_BRANCH], repo).stdout.strip() + assert main_before + + # The change the AI partner made, sitting in the working tree. + (repo / "frontend" / "src" / "copy.ts").write_text( + "export const COPY = 'caught itself three times'\n", encoding="utf-8" + ) + + result = implement( + proposal.number, root=repo, proposals_dir=proposals, push=False, now=STAMP, + runner=lambda cmd, cwd: _run(list(cmd), cwd), + ) + + main_after = _run(["git", "rev-parse", PROTECTED_BRANCH], repo).stdout.strip() + assert main_after == main_before, "implement moved main" + + assert result.committed + assert result.branch == f"{BRANCH_PREFIX}{proposal.number}" + assert current_branch(lambda cmd, cwd: _run(list(cmd), cwd), repo) != PROTECTED_BRANCH + + # The commit exists on the prop branch and NOT on main. + on_main = _run(["git", "branch", "--contains", result.commit_sha], repo).stdout + assert PROTECTED_BRANCH not in on_main.replace("*", "").split() + + +def test_implement_writes_the_report_into_the_proposal(repo: Path) -> None: + proposals = repo / "docs" / "proposals" + proposal = _proposal(affected_paths=["frontend/src/copy.ts"], risk_class="content") + path = write_proposal(proposal, proposals_dir=proposals, generated_at=STAMP) + (repo / "frontend" / "src" / "copy.ts").write_text("changed\n", encoding="utf-8") + + implement(proposal.number, root=repo, proposals_dir=proposals, push=False, now=STAMP, + runner=lambda cmd, cwd: _run(list(cmd), cwd)) + + text = path.read_text(encoding="utf-8") + assert "## Implementation report" in text + assert "### Diff stat" in text + assert "### Gates" in text + assert "STOPPED HERE" in text + assert "Daniel merges via pull request" in text + + +def test_report_in_the_file_never_claims_it_was_not_committed(repo: Path) -> None: + """The durable artifact must not state the opposite of what happened. + + Regression for the first dogfood run: `write_report` runs BEFORE the commit (the + report is part of what gets committed), so `committed`/`pushed` were still False and + were rendered into the proposal as `committed: **False**` on a run that had in fact + committed and pushed. The file now reports only what is known at write time. + """ + proposals = repo / "docs" / "proposals" + proposal = _proposal(affected_paths=["frontend/src/copy.ts"], risk_class="content") + path = write_proposal(proposal, proposals_dir=proposals, generated_at=STAMP) + (repo / "frontend" / "src" / "copy.ts").write_text("changed\n", encoding="utf-8") + + result = implement(proposal.number, root=repo, proposals_dir=proposals, push=False, + now=STAMP, runner=lambda cmd, cwd: _run(list(cmd), cwd)) + assert result.committed, "precondition: this run should have committed" + + text = path.read_text(encoding="utf-8") + assert "committed: **False**" not in text + assert "pushed: **False**" not in text + assert "commit and push: performed immediately after" in text + # The console rendering, by contrast, is finalised and may state both. + assert result.finalised + assert "committed: **True**" in result.render() + + +def test_implement_refuses_a_diff_that_touches_a_firewall_path(repo: Path) -> None: + """Defence in depth: the DOCUMENT said frontend, the DIFF says risk limits.""" + proposals = repo / "docs" / "proposals" + proposal = _proposal(affected_paths=["frontend/src/copy.ts"], risk_class="content") + write_proposal(proposal, proposals_dir=proposals, generated_at=STAMP) + + (repo / "config").mkdir() + (repo / "config" / "risk.yaml").write_text("max_drawdown: 0.99\n", encoding="utf-8") + + result = implement(proposal.number, root=repo, proposals_dir=proposals, push=False, + now=STAMP, runner=lambda cmd, cwd: _run(list(cmd), cwd)) + + assert result.aborted + assert not result.committed + assert not result.firewall_ok + assert "firewall" in result.aborted.lower() + + +def test_implement_aborts_on_an_empty_diff(repo: Path) -> None: + proposals = repo / "docs" / "proposals" + proposal = _proposal(affected_paths=["frontend/src/copy.ts"], risk_class="content") + write_proposal(proposal, proposals_dir=proposals, generated_at=STAMP) + # Commit the proposal so the tree is clean and the diff is genuinely empty. + _run(["git", "add", "-A"], repo) + _run(["git", "commit", "-m", "proposal"], repo) + + result = implement(proposal.number, root=repo, proposals_dir=proposals, push=False, + now=STAMP, runner=lambda cmd, cwd: _run(list(cmd), cwd)) + assert "empty" in result.aborted + + +def test_branch_guard_refuses_when_not_on_the_prop_branch(repo: Path) -> None: + """The guard itself, exercised directly.""" + from quantlab.improve.implement import _assert_on_prop_branch + + runner = lambda cmd, cwd: _run(list(cmd), cwd) # noqa: E731 + with pytest.raises(NotOnBranch) as caught: + _assert_on_prop_branch(runner, repo, 99) + assert PROTECTED_BRANCH in str(caught.value) + + +def test_no_auto_merge_path_exists_in_the_source() -> None: + """The structural proof: read implement.py and assert no merge verb is present. + + This catches the case the behavioural test cannot — someone later ADDING a merge, + where the old test would still pass because it only asserts about the runs it makes. + """ + from quantlab.improve import implement as module + + source = Path(module.__file__).read_text(encoding="utf-8") + # Strip the prose, which legitimately discusses merging at length. + code = "\n".join( + line for line in source.splitlines() + if not line.lstrip().startswith("#") + ) + code = code.split('"""')[0] + '"""'.join(code.split('"""')[2:]) + + forbidden = [ + '"merge"', "'merge'", '"rebase"', "'rebase'", '"cherry-pick"', + f'"origin", "{PROTECTED_BRANCH}"', f'"{PROTECTED_BRANCH}"]', + "--ff", "gh pr merge", '"pr", "merge"', + ] + hits = [f for f in forbidden if f in code] + assert not hits, f"an automated merge path appeared in implement.py: {hits}" + + +def test_implement_and_propose_agree_on_the_firewall(repo: Path) -> None: + """One firewall, two call sites — not two copies that can drift apart.""" + from quantlab.improve import implement as impl_mod + from quantlab.improve import propose as prop_mod + + assert impl_mod.firewall is firewall + assert prop_mod.firewall is firewall diff --git a/tests/test_path_anchoring.py b/tests/test_path_anchoring.py new file mode 100644 index 0000000..ea68097 --- /dev/null +++ b/tests/test_path_anchoring.py @@ -0,0 +1,128 @@ +"""Every default artifact path must be anchored to the repo root, never to the CWD. + +WHY THIS FILE EXISTS. On 2026-08-14 the first UNATTENDED `glassbox refresh` aborted with +`PermissionError: [WinError 5] Access is denied: 'reports'`. Nothing had a handle on the +project's `reports/` tree; the message named a directory that had never existed. The +scheduled task supplies no working directory, so the process ran in `C:\\Windows\\System32`, +and `snapshot.DEFAULT_REPORT_DIR` was a bare relative `Path("reports") / "glassbox"`. +`mkdir(parents=True)` recursed into the missing parent and tried to create +`C:\\Windows\\System32\\reports`, which is denied. + +The defect was invisible for weeks because every READ goes through `GlassboxPaths`, which +derives from `PROJECT_ROOT`, and because every manual run happened to start in the repo +root. Only the unattended path had a different CWD, and there was exactly one of those. + +These tests therefore assert the INVARIANT rather than the incident: a default path that +resolves differently depending on where the process was started is a bug, whether or not +it currently crashes. The `verify_dist` case is the sharper one — a missing `.env` there +is not an error, so a CWD-relative default degrades the gate to a silent no-op that still +reports PASS. +""" + +from __future__ import annotations + +import ast +import os +from pathlib import Path + +import pytest + +from quantlab.constants import PROJECT_ROOT +from quantlab.glassbox.serve import DEFAULT_SNAPSHOT_DIR +from quantlab.glassbox.snapshot import DEFAULT_REPORT_DIR +from quantlab.glassbox.verify_dist import DEFAULT_ENV_PATH + +# Every module-level default that names a location on disk. +ANCHORED_DEFAULTS = { + "snapshot.DEFAULT_REPORT_DIR": DEFAULT_REPORT_DIR, + "serve.DEFAULT_SNAPSHOT_DIR": DEFAULT_SNAPSHOT_DIR, + "verify_dist.DEFAULT_ENV_PATH": DEFAULT_ENV_PATH, +} + + +@pytest.mark.parametrize("name", sorted(ANCHORED_DEFAULTS)) +def test_default_path_is_absolute(name: str) -> None: + """A relative default is resolved against the CWD, which no caller controls.""" + assert ANCHORED_DEFAULTS[name].is_absolute(), ( + f"{name} is relative; under the scheduler it resolves against " + f"C:\\Windows\\System32, not the repo" + ) + + +@pytest.mark.parametrize("name", sorted(ANCHORED_DEFAULTS)) +def test_default_path_lives_under_project_root(name: str) -> None: + assert ANCHORED_DEFAULTS[name].is_relative_to(PROJECT_ROOT), ( + f"{name} resolves outside the repo root {PROJECT_ROOT}" + ) + + +@pytest.mark.parametrize("name", sorted(ANCHORED_DEFAULTS)) +def test_default_path_does_not_move_with_the_cwd( + name: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """The reproduction, made cheap: run from a foreign CWD and re-resolve. + + `monkeypatch.chdir` stands in for the scheduler's `C:\\Windows\\System32`. Before the + fix, `Path("reports") / "glassbox"` resolved under `tmp_path` here and under the repo + when pytest was started from the repo root — the same expression naming two different + directories is exactly the defect. + """ + before = ANCHORED_DEFAULTS[name].resolve() + monkeypatch.chdir(tmp_path) + assert Path(ANCHORED_DEFAULTS[name]).resolve() == before + assert Path.cwd() != PROJECT_ROOT, "the test did not actually leave the repo root" + + +def test_no_bare_relative_path_defaults_in_glassbox() -> None: + """A structural guard: catch the NEXT one, not just the three we found. + + Parses every module in the package and fails on any module-level assignment whose + value is a `Path("literal")` — with or without `/` operands chained onto it — that + does not start from an anchored name. Assertions on the three known constants above + only prove today's fix; this proves the class stays closed. + """ + package = Path(PROJECT_ROOT) / "src" / "quantlab" / "glassbox" + anchors = {"PROJECT_ROOT", "CONFIG_DIR"} + offenders: list[str] = [] + + def base_of(node: ast.expr) -> ast.expr: + """Walk down the left spine of a `a / b / c` chain to its leftmost operand.""" + while isinstance(node, ast.BinOp) and isinstance(node.op, ast.Div): + node = node.left + return node + + for module in sorted(package.glob("*.py")): + tree = ast.parse(module.read_text(encoding="utf-8"), filename=str(module)) + for node in tree.body: # module level only; function-local paths are the + if not isinstance(node, ast.Assign): # caller's business, not a default + continue + base = base_of(node.value) + if not (isinstance(base, ast.Call) and getattr(base.func, "id", "") == "Path"): + continue + if not base.args or not isinstance(base.args[0], ast.Constant): + continue + literal = base.args[0].value + if not isinstance(literal, str) or Path(literal).is_absolute(): + continue + targets = ", ".join( + t.id for t in node.targets if isinstance(t, ast.Name) + ) + offenders.append(f"{module.name}:{node.lineno} {targets} = Path({literal!r})") + + assert not offenders, ( + "module-level path default(s) resolved against the CWD instead of an anchor " + f"({' or '.join(sorted(anchors))}):\n " + "\n ".join(offenders) + ) + + +def test_scheduled_entry_points_do_not_depend_on_cwd(monkeypatch: pytest.MonkeyPatch, + tmp_path: Path) -> None: + """Importing the scheduled chain from a foreign CWD must still find the repo. + + `PROJECT_ROOT` derives from `__file__`, so this holds by construction — the test + pins it, because the whole fix rests on that one property. + """ + monkeypatch.chdir(tmp_path) + assert PROJECT_ROOT.is_absolute() + assert (PROJECT_ROOT / "src" / "quantlab" / "constants.py").is_file() + assert Path(os.getcwd()) != PROJECT_ROOT From 8a192ad7d928214a211f7f709417ba8939b612e0 Mon Sep 17 00:00:00 2001 From: danielfmonzon <123423019+danielfmonzon@users.noreply.github.com> Date: Sat, 15 Aug 2026 13:59:45 -0400 Subject: [PATCH 2/3] snapshot: commit the outstanding public captures These accumulated across the 08-14 and 08-15 refreshes. Committing them clears the provenance warning: the chain records the commit it deployed from, and a dirty tree meant the published bytes cited a commit that did not contain them. --- frontend/public/snapshot/api-decisions.json | 47 +- .../api-divergence--label-crypto_trend.json | 15 + ...pi-divergence--label-crypto_voltarget.json | 15 + .../snapshot/api-divergence--label-trend.json | 17 + .../api-divergence--label-voltarget.json | 17 + frontend/public/snapshot/api-divergence.json | 64 + .../api-equity--label-crypto_trend.json | 36 +- .../api-equity--label-crypto_voltarget.json | 36 +- .../snapshot/api-equity--label-trend.json | 26 +- .../snapshot/api-equity--label-voltarget.json | 26 +- frontend/public/snapshot/api-equity.json | 124 +- frontend/public/snapshot/api-overview.json | 54 +- frontend/public/snapshot/api-risk.json | 14 +- .../api-runs--label-crypto_trend.json | 317 ++++- .../api-runs--label-crypto_voltarget.json | 396 +++++- .../snapshot/api-runs--label-trend.json | 262 +++- .../snapshot/api-runs--label-voltarget.json | 285 +++- ...crypto_trend_20260811T111731Z-narrate.json | 34 + ...crypto_trend_20260812T123719Z-narrate.json | 34 + ...crypto_trend_20260813T012741Z-narrate.json | 34 + ...crypto_trend_20260814T063810Z-narrate.json | 34 + ...crypto_trend_20260815T003005Z-narrate.json | 34 + ...to_voltarget_20260811T111735Z-narrate.json | 86 ++ ...to_voltarget_20260812T123722Z-narrate.json | 66 + ...to_voltarget_20260813T012744Z-narrate.json | 86 ++ ...to_voltarget_20260814T063813Z-narrate.json | 86 ++ ...to_voltarget_20260815T003008Z-narrate.json | 66 + ...ns-run_trend_20260811T140012Z-narrate.json | 51 + ...ns-run_trend_20260812T140032Z-narrate.json | 51 + ...ns-run_trend_20260813T140007Z-narrate.json | 51 + ...ns-run_trend_20260814T140007Z-narrate.json | 51 + ...un_voltarget_20260811T140005Z-narrate.json | 86 ++ ...un_voltarget_20260812T140025Z-narrate.json | 66 + ...un_voltarget_20260813T140004Z-narrate.json | 66 + ...un_voltarget_20260814T140004Z-narrate.json | 66 + frontend/public/snapshot/api-runs.json | 1254 ++++++++++++++++- frontend/public/snapshot/api-timeline.json | 210 ++- frontend/public/snapshot/manifest.json | 186 ++- 38 files changed, 4368 insertions(+), 81 deletions(-) create mode 100644 frontend/public/snapshot/api-runs-run_crypto_trend_20260811T111731Z-narrate.json create mode 100644 frontend/public/snapshot/api-runs-run_crypto_trend_20260812T123719Z-narrate.json create mode 100644 frontend/public/snapshot/api-runs-run_crypto_trend_20260813T012741Z-narrate.json create mode 100644 frontend/public/snapshot/api-runs-run_crypto_trend_20260814T063810Z-narrate.json create mode 100644 frontend/public/snapshot/api-runs-run_crypto_trend_20260815T003005Z-narrate.json create mode 100644 frontend/public/snapshot/api-runs-run_crypto_voltarget_20260811T111735Z-narrate.json create mode 100644 frontend/public/snapshot/api-runs-run_crypto_voltarget_20260812T123722Z-narrate.json create mode 100644 frontend/public/snapshot/api-runs-run_crypto_voltarget_20260813T012744Z-narrate.json create mode 100644 frontend/public/snapshot/api-runs-run_crypto_voltarget_20260814T063813Z-narrate.json create mode 100644 frontend/public/snapshot/api-runs-run_crypto_voltarget_20260815T003008Z-narrate.json create mode 100644 frontend/public/snapshot/api-runs-run_trend_20260811T140012Z-narrate.json create mode 100644 frontend/public/snapshot/api-runs-run_trend_20260812T140032Z-narrate.json create mode 100644 frontend/public/snapshot/api-runs-run_trend_20260813T140007Z-narrate.json create mode 100644 frontend/public/snapshot/api-runs-run_trend_20260814T140007Z-narrate.json create mode 100644 frontend/public/snapshot/api-runs-run_voltarget_20260811T140005Z-narrate.json create mode 100644 frontend/public/snapshot/api-runs-run_voltarget_20260812T140025Z-narrate.json create mode 100644 frontend/public/snapshot/api-runs-run_voltarget_20260813T140004Z-narrate.json create mode 100644 frontend/public/snapshot/api-runs-run_voltarget_20260814T140004Z-narrate.json diff --git a/frontend/public/snapshot/api-decisions.json b/frontend/public/snapshot/api-decisions.json index afab4f3..f544e58 100644 --- a/frontend/public/snapshot/api-decisions.json +++ b/frontend/public/snapshot/api-decisions.json @@ -1,6 +1,51 @@ { - "count": 31, + "count": 40, "entries": [ + { + "date": "2026-08-15", + "title": "An unchecked secret gate is an abort, not a note (automated chain only)", + "body": "**Decision.** In the **automated** refresh chain, an env-secret status of NOT CHECKED\naborts at the deploy decision exactly as a redaction does: bytes held, WARNING raised,\nhuman pre-review required. Interactive runs (`--interactive`) and `--dry-run` keep the\nexisting note and proceed.\n\n**Rationale \u2014 direct consequence of Defect #2** (the 2026-08-15 path-anchoring audit).\n`verify_dist`'s `.env` fallback was CWD-relative, and a missing `.env` is not an error in\n`load_env_secret_prefixes`: it returns an empty prefix set with a note and the gate still\nreports **PASS**. Under the scheduler, which supplies no working directory, that\ncombination would have published bytes whose env-secret half had searched for nothing\nwhile the chain report said the gate passed. The path bug is fixed; this closes the\nclass. **\"The check silently did not run\" must never be indistinguishable from \"the check\nran and found nothing\"** \u2014 that is the same failure shape as Defect #1, where an error\nnaming `reports` described a directory that had never existed.\n\n**Why the two modes differ.** The abort exists to substitute for a reader who is absent,\nso it fires exactly when the reader is. `--dry-run` publishes nothing and an interactive\noperator sees the note in the report they are already reading. The default is\n`automated=True` \u2014 fail closed, so a run that did not declare a human is assumed not to\nhave one. The env-secret status is now printed on **every** report, passing or not,\nbecause \"the check ran\" should be confirmable rather than inferred from the absence of a\nnote." + }, + { + "date": "2026-08-15", + "title": "The AI improvement pipeline, and the firewall that makes it safe", + "body": "**Decision.** An automated improvement loop is added in three parts:\n`quantlab propose` (analysis, writes a document), a structural **firewall** (a denylist\nof frozen paths and change classes), and `quantlab implement PROP-n` (applies on a\nbranch, gates it, reports, pushes, stops). Merge is human-only. The loop may improve the\nmachine around the strategy; it may never touch the strategy, the limits that constrain\nit, the instruments that measure it, or the gate that keeps secrets out of the published\nbytes.\n\n**Why this needs a firewall at all.** The failure mode is not a rogue model, it is a\n*reasonable* one. Point an improvement loop at this repository and the most persuasive\nproposal available to it is to widen the 50 bps divergence threshold: four of the last\nsix weekly reviews fired DIVERGING, every one was later attributed to mark-phase\ngeometry, and the alerts produced no action. That argument is evidenced, coherent, and\nexactly wrong \u2014 it is the 2026-08-10 ruling (\"fix the instrument, not the threshold\")\nrun in reverse. A prompt asking a model not to do that is a request. This is a gate.\n\n**What `propose` may READ** \u2014 an allowlist, in `improve/sources.py`: weekly reviews,\ndaily digests, per-run paper reports, `alerts.jsonl`, the CI workflow, Glass Box / site\nartifacts, `decisions.md`, and prior proposals. **Source code is deliberately not\nreadable evidence.** An observation has to trace to an artifact this system published\nabout itself; a proposal justified by reading the implementation is how you end up\n\"fixing\" a measurement to agree with the code rather than the other way round. Absent\nsources are reported as absent rather than silently skipped.\n\n**What no proposal may CHANGE** \u2014 the firewall, in `improve/firewall.py`, enforced at\nBOTH ends. Forbidden paths:\n\n| path | why |\n|---|---|\n| `src/quantlab/backtest/strategies/` | strategy definitions and literature-fixed parameters |\n| `src/quantlab/broker/` | the order path, frozen under human review |\n| `src/quantlab/risk/` | limits, kill-switch, halt state |\n| `config/risk.yaml`, `config/crypto_risk.yaml` | risk limit values |\n| `src/quantlab/glassbox/sanitize.py` | sanitizer patterns \u2014 the secret-leak gate |\n| `src/quantlab/scheduling/tasks.py` | schedule cadences |\n\nPaths alone are not enough \u2014 a threshold can be moved from a constant in a reporting\nmodule \u2014 so six forbidden **change classes** are also matched against the proposal's own\nprose: `strategy_parameters`, `risk_limits`, `threshold_constants`, `schedule_cadence`,\n`sanitizer_patterns`, `broker_logic`. A class fires only when a frozen TARGET and a\nmutation VERB co-occur, because a target alone is discussion and a verb alone is most of\nEnglish. Matching is word-boundary anchored: naive substring matching fired `change`\ninside \"un**change**d\" and refused the sentence \"the threshold value itself is\nunchanged\" \u2014 a firewall that fires on its own disclaimers trains its operator to route\naround it, the same lesson as the 2026-07-26 narrowing of `apca_api_header`.\n\n**Refusals cite the iron rule verbatim** and state that no flag overrides them, because\na refusal that does not explain itself is indistinguishable from a bug. A refused\nproposal writes **nothing** \u2014 the gate runs before the file is created, mirroring the\nsnapshot writer's \"nothing is written unless the gate passes\".\n\n**Strategy-performance data may inform INFRASTRUCTURE proposals only.** The rule is\nenforced on the proposal's EFFECT, not on what it read. Reading the divergence figures\nto notice that the Glass Box exposes only the raw number and not the residual the verdict\nwas taken on is the intended use. Reading the same figures to argue for a different\nlookback is refused.\n\n**Merge is human-only, and that is structural too.** `implement` creates `prop/{n}`,\napplies the change, re-runs the firewall **against the actual diff** (the document and\nthe diff are different artifacts, and only the second becomes a commit), runs\nruff/mypy/pytest plus the frontend suite and `verify-dist` when the site is touched,\nwrites an implementation report into the proposal, commits, pushes, and ends. There is\nno `git merge`, no `git rebase`, and no push to `main` anywhere in it. Two tests prove\nthis rather than one: a behavioural test runs the real command against a real repository\nand asserts `main`'s SHA is byte-identical afterwards, and a source-level test reads\n`implement.py` and fails if a merge verb ever appears in it. The first catches a bug; the\nsecond catches a future feature. **Daniel merges via pull request after Quant Lead\nreview.**\n\nA loop that can merge its own work is trusted by construction, and every safety property\ndownstream of it collapses into \"the analysis was right\" \u2014 the one thing that cannot be\nguaranteed. Keeping the last step human means the worst case of a wrong proposal is a\nbranch nobody merges.\n\n**Dogfooded on PROP-1.** The first real proposal was the Story page's claim that the\nsystem had caught itself \"twice\"; there have been three self-caught measurement\nincidents (the 2026-07-22 scheduler leak, the 2026-07-25 partial-bar read, and the\n2026-08-10 comparator interval dating defect). Proposed, firewall-passed, implemented on\n`prop/1`, six gates green, pushed, stopped at the human gate. The run also found two real\ndefects in the pipeline itself, both fixed with regression tests: `implement` staged\n`-A` in patch mode (which would have swept unrelated working-tree changes into a\nproposal's commit), and the report written into the proposal claimed\n`committed: False` on a run that had committed and pushed, because the report is written\nbefore the commit that carries it." + }, + { + "date": "2026-08-15", + "title": "DMARC enforcement: `p=none` -> `p=quarantine`", + "body": "**Decision.** The `_dmarc.danielmonzonautomation.com` TXT record moves from\nmonitor-only to enforcing:\n\n```\nv=DMARC1; p=quarantine; sp=quarantine; rua=mailto:; fo=1\n```\n\n`sp=quarantine` is stated explicitly rather than left to inherit, so a subdomain\ncannot become an unenforced sending path by omission. `rua` and `fo=1` are\nunchanged \u2014 aggregate reporting continues, and enforcement is not a reason to\nstop reading it.\n\nThe `rua` mailbox is elided above rather than transcribed, because this file is\npublished verbatim through `/api/decisions` and `email_address` is a **forbidden**\npattern in the snapshot gate \u2014 not a redaction, a hard refusal. Writing the literal\naddress here would fail-close the next automated refresh on the gate's own\ndocumentation. The live value is in the zone; it is unchanged from the `p=none`\nrecord it replaced.\n\n**Rationale \u2014 the observation window did its job.** `p=none` existed to answer one\nquestion: does anything legitimate send as this domain that would break under\nenforcement? The aggregate reports answer no. **10 of 11** reported messages were\nDMARC-aligned. The single failure originated from **AWS** infrastructure with no\nalignment on either SPF or DKIM \u2014 not a legitimate sender misconfigured, but\nexactly the unauthorised use of the domain that a policy is supposed to act on.\nQuarantining that message is the correct outcome, not collateral damage. A policy\nwhose only enforcement effect is the thing you deployed it for has no migration\nrisk left to buy down by waiting.\n\n**Preconditions verified in the zone before the change**, since enforcement is only\nsafe if alignment is actually achievable: MX is Google (`SMTP.GOOGLE.COM`, pref 1),\nSPF is `v=spf1 include:_spf.google.com ~all`, and a valid `google._domainkey`\nDKIM RSA key is published. Daniel confirmed **Gmail-only sending**; the zone\ncontents corroborate it \u2014 there is no second sending path to break.\n\n**`quarantine`, not `reject`.** Quarantine is recoverable by the recipient: a\nfalse positive lands in spam where it can still be retrieved. Reject is\nunrecoverable and bounces. With one enforcing week of evidence and a\nsingle-digit message sample, the recoverable rung is the honest one. `p=reject`\nis a later decision that should be taken on aggregate reports gathered *under*\nquarantine, not on reports gathered under `none`.\n\n**Operational note \u2014 Netlify DNS cannot modify a record in place.** The API\nexposes `createDnsRecord` and `deleteDnsRecord` and **no update method**, so\n\"edit the record\" is necessarily delete-then-create. Order matters and is not\narbitrary: deleting first leaves a brief window with *no* DMARC record, which\nreceivers treat as no policy \u2014 fail-open, mail flows. Creating first would leave\ntwo `_dmarc` records, and RFC 7489 \u00a76.6.3 requires receivers to apply **no\npolicy at all** when more than one is present \u2014 a worse state that also silently\ndefeats the change. Delete-then-create is therefore the correct order, and the\nrecord ID changes as a consequence (`6a6645686c76095d7a08d75a` ->\n`6a808f7c09c4e9aa73343cf1`).\n\n**Change protocol, run in full.** MX resolved before and after and compared \u2014\nunchanged (`pref=1 smtp.google.com`), with the MX record object itself untouched\n(same id `6a2bffbd9e417c2f6941fd77`). Exactly one `_dmarc` record confirmed both\nbefore and after, at the API level and at **all four** authoritative\nnameservers (`dns{1..4}.p08.nsone.net`), byte-compared against the intended\nvalue. TTL 3600, so resolver caches carry the old `p=none` for up to an hour;\nthat staleness is expected and is not a failed change." + }, + { + "date": "2026-08-10", + "title": "One bounded retry, and the list of things that must never get one", + "body": "**The gap.** Any abort ended the attempt until the next scheduled day. A vendor publishing a\nbar three minutes after 10:00 cost a whole session of paper record \u2014 and those gaps are\nprecisely what the divergence work kept tripping over, because a missed run turns two clean\n24h mark windows into a 70h one and a 6.92h one and leaves a shadow session with no paper\ncounterpart.\n\n**Decision.** `run_paper_with_retry` runs the full gated pipeline again, **once**, ten\nminutes later, if and only if the first abort's cause was transient. Bounded deliberately:\nthe point is to survive a late bar or a blipped read, not to keep hammering. If ten minutes\ndoes not cure it, the condition is real and the next scheduled run is soon enough.\n\n**Retryability is decided AT THE ABORT SITE**, not pattern-matched from the reason string\nafterwards. Each `_abort` call passes `retryable=`, so the judgement is made where the\nexception and the stage are both in hand, and `PaperRunReport` carries `abort_retryable`\nalongside `attempt=1|2`.\n\n**Retried:**\n\n| stage | why |\n|---|---|\n| `ingest` | transient network/API failure; the ingest is an upsert, so repeating it is idempotent |\n| `health` | `FREEZE_STALE_DATA` \u2014 the canonical case: the bar had not been published yet |\n| `account` | only a sustained transport/5xx on the READ path; the client has already exhausted its own tenacity policy by the time this surfaces |\n\n**Never retried, each for its own reason:**\n\n| stage | why not |\n|---|---|\n| `risk_state` halted | a decision, not a hiccup; only `risk reset` clears it |\n| `validate` | a data-CONTENT error \u2014 a re-run reads the same bars and reaches the same verdict |\n| `account` blocked / non-positive equity | a real account state, not a blip |\n| `account` permanent fault | `DataError`/`TradingError` means the API answered and the answer was bad \u2014 auth, permissions, malformed body |\n| `ingest` `ConfigError` | a missing key is not cured by waiting |\n| `target_weights` | no usable history is a content condition |\n| `evaluate_portfolio` | a HALT/KILL was just **written**; a retry must never look like a second chance at trading |\n| `submit` | submission has **begun** and orders may be live. A partial submission alerts, always |\n\nThe classification leans on the client's public exception types rather than its internals:\n`TradingError` subclasses `DataError`, and `_RetryableError` does not, so `not\nisinstance(exc, DataError)` cleanly separates \"sustained transport failure\" from \"the API\nsaid no\". **The client's tenacity policy is untouched** \u2014 this wraps at the runner level, as\nscoped.\n\n**Two properties worth pinning.** Alerting: attempt 1's abort alert is buffered and\n*discarded* if the retry supersedes it, so one logical failure raises exactly one WARNING \u2014\nthe final outcome's \u2014 and a recovered run raises none at all. Equity snapshots: every\nretryable abort occurs at stages (b)\u2013(e), strictly before the snapshot is appended at (h),\nso a retry can never double-write a mark.\n\n**A guarantee strengthened along the way.** The retry needs a fresh broker per attempt, so\n`broker_factory` was threaded through \u2014 and passing it into `run_paper` (rather than calling\nit in the wrapper) turned out to matter: the runner builds the client at stage (e), so a\nhalted account still aborts with **no broker constructed and no credentials read at all**.\nThe first draft called the factory eagerly and quietly broke that documented property; the\ntests now assert `factory_calls == 0` for every pre-broker abort, which is a stronger\nstatement than the pipeline previously made anywhere." + }, + { + "date": "2026-08-10", + "title": "The watchdog: making silence audible", + "body": "**The failure mode.** Everything runs from Task Scheduler on one workstation, and\n`StartWhenAvailable` cannot run anything while that machine is **off**. On 2026-08-01 no\ncrypto run fired. Nothing alerted \u2014 because nothing failed. The pipeline was simply never\ninvoked, and a system that only reports on the runs it performs is structurally blind to the\nruns it did not.\n\n**Decision.** The daily digest now asks the opposite question: which firings *should* have\nhappened since the last digest, and is there an artifact for each? Missing ones render a\n**MISSED RUNS** section and fire **exactly one** WARNING naming them all. One alert rather\nthan one per miss: a machine off for a weekend misses a dozen firings for a single reason,\nand a dozen alerts would bury the reason in the noise.\n\nEvidence per task \u2014 a run report (aborted counts: the task *fired*, and its abort already\nalerted on its own), a `week_*.json`, or a `glassbox.refresh` alert record, which the chain\nwrites on success and abort alike.\n\n**Two design points that decide whether it is useful or ignored.** `scheduling.tasks.SCHEDULE`\nis the single source of truth for when each task fires, so a schedule change cannot leave the\nexpectation behind. And **not-yet-due is not missing**: the digest runs at 16:45 ET, before\nthe crypto run (20:30), the weekly (17:00) and the refresh (17:30), so every expectation is\ngated on its scheduled instant having passed. Without that gate it would report three missed\nfirings every weekday, and a watchdog that cries wolf on schedule is worse than none. Market\nholidays and weekends are excluded for equities via the calendar, never a weekday count.\n\n**It found four real gaps on its first run against the repository's own history** (12-day\nlookback): the 2026-08-01 crypto pair, and \u2014 unprompted \u2014 the **2026-07-31 weekly review**,\nwhich was never generated on its Friday at all (the published `week_20260802` was a Sunday\ncatch-up), plus the 07-31 and 08-07 refreshes, which predate the task existing. The 07-31\nweekly is the same host-off weekend as the crypto miss, and nothing had previously noticed it." + }, + { + "date": "2026-08-10", + "title": "CI is healthy, and a provenance warning that does not block", + "body": "**CI verification.** All 12 CI runs in the workflow's life are accounted for; the last 10\npushes each ran it. **11 success, 1 failure.** The failure \u2014 run `31437620995`, commit\n`8a7f6ed` \u2014 was the freshness time-bomb in\n`test_a_complete_and_clean_dist_passes_the_whole_gate`, which was knowingly left failing at\nthat commit and fixed in `5861a72`; the next run is green. **CI was never failing silently\nand no push skipped it.** Two commits (`ac4d27a`, `b59bb8d`, both 2026-07-22) show no run of\ntheir own because they were pushed together with `15db817` \u2014 GitHub runs the workflow once\nper push, at its head, which is expected rather than a gap. Local runs report 570 passed\nwhere CI reports 565 passed + 5 skipped: the five `live`-marked tests need `TIINGO_API_KEY`,\nwhich exists locally and not in CI.\n\n**Decision: a dirty-tree check on the two commands that act outward**, `glassbox refresh` and\n`schedule install`. Both are otherwise happy to act from uncommitted edits, and the artifacts\nthey leave record a commit \u2014 the snapshot manifest carries `git_commit`, every report carries\n`version_string()`. From a dirty tree that recorded commit is a claim the repository cannot\nsubstantiate: the published figures came from code that exists nowhere but one laptop.\n`schedule install` is the sharper case, because the tasks it writes will run *that checkout*\nunattended for months.\n\n**Report-only, and that is the considered choice, not a shortcut.** A hard block would be\nthe wrong trade for a transparency site whose staleness is the bigger risk \u2014 refusing to\npublish a fresh snapshot because a README line is unstaged would reintroduce the\nfifteen-day-stale failure in order to protect a provenance detail. So it warns, records the\nwarning in the run report (`RefreshResult.repo`, rendered under `PROVENANCE`), prints it\nbefore the chain starts, and proceeds. Everything is best-effort: no git, a detached HEAD or\nno upstream produces a note rather than an error, because none of those should stop a\nscheduled task.\n\n**A self-inflicted store corruption, and the isolation defect behind it.** Making the broker\nlazy had a consequence I did not anticipate. `tests/test_late_run_guard.py` proves the cutoff\nguard lets a run through by stubbing `_trading_client_for` and asserting it gets called \u2014 and\nbecause the broker used to be built at the *top* of `_run_one_paper`, that stub fired before\nany pipeline stage ran. With the broker built at stage (e), stages (b)\u2013(d) now really\nexecute, so those three tests began performing a **real vendor ingest that upserted into the\nproduction `data/eod` store**. One run wrote a partial same-day SPY bar with no matching IEF\nbar; the resulting internal gap made `build_price_panel` raise, which broke the digest and\nwould have aborted every subsequent `trend` run at `validate`.\n\nI initially misread that as an environmental data condition and re-ingested IEF to clear it\n(the vendor had published by then, so the store is correct). It was not environmental \u2014 my\nown test run caused it, and the ordering change is what exposed it. The fixture now stubs\n`_clock_for` and `_ingest_fn_for` as well, the three tests no longer touch the network or the\nstore (verified by hashing both parquet files across a run), and the suite went from 5.8s to\n0.7s. **A guard test must never write to the production store**, and this one silently could.\n\nCI caught the ordering change independently, as a `ConfigError` on a keyless runner: with the\nbroker no longer first, `_clock_for` became the first credentialed call. The fix was verified\nagainst a simulated keyless environment rather than only locally, because passing on a\nmachine that has `.env` proves nothing about the runner that does not.\n\n**One further isolation leak, recorded not fixed.** The suite also appends to the real\n`reports/logs/quantlab.jsonl`. Harmless today \u2014 the log is append-only and nothing reads it\nfor decisions \u2014 but it is the reason forensics on the store corruption above took longer than\nit should have, since the log was full of test-generated `paper_run` events. Out of scope\nhere; cheap, and worth picking up next." + }, + { + "date": "2026-08-10", + "title": "Automated Glass Box refresh, and the doctrine that lets a machine deploy", + "body": "**The failure being fixed.** The deploy ritual was four commands across two directories, run\nby hand. So it was not run: the published site sat at its **2026-07-26** snapshot for\n**fifteen days** while the trading record moved underneath it, and the completeness gate's\nown 14-day freshness limit had been breached for a day before anyone noticed. A site whose\nentire argument is \"you can check this\" was showing figures two weeks stale. Manual is the\ndefect; the ritual is now `quantlab glassbox refresh`, scheduled.\n\n**The chain is fail-closed.** `snapshot -> build:public -> verify-dist -> netlify deploy\n--prod`, each step running only if every earlier one passed, and the report naming where it\nstopped. The site ID is **pinned in code** (`be63f48c-\u2026`) for the reason\n`frontend/README.md` already pinned it on the command line: `build:public` recreates\n`frontend/.netlify/`, dropping the link, and an unlinked `netlify deploy` opens an\ninteractive \"Link this directory?\" prompt \u2014 inside an unattended chain that is worse than an\nerror, because it blocks forever or exits having deployed nothing while looking like it ran.\n\n**DOCTRINE AMENDMENT (Quant Lead ruling).** The standing rule was that a human reads the\nsanitization report before any deploy. That rule is preserved where it matters and relaxed\nonly where the machine's judgement is total:\n\n* automated deploy proceeds **only** on gate PASS with **zero forbidden matches AND zero\n redactions** \u2014 nothing found, and nothing that had to be scrubbed;\n* **any** redaction aborts before deploy with a WARNING for human pre-review, even though\n the writer has already scrubbed it. The scrub working is not the question; a redaction\n means the capture *contained* something unpublishable, and that is precisely what a human\n should see before those bytes go out;\n* **every** run emails the full report, deployed or not \u2014 INFO on deploy, WARNING on abort.\n A gate whose output is only read after a failure trains its operator to ignore it.\n\nThe asymmetry is the whole ruling: automation may publish a clean build, and may never\npublish one that needed cleaning.\n\n**Scheduled** as `quantlab-glassbox-refresh`, Fridays **17:30** local, via the existing\ninstall machinery \u2014 `install`/`uninstall`/`show` now cover four tasks, and the\n`StartWhenAvailable` post-step gives it the same missed-start catch-up as the others. 17:30\nputs it half an hour after the weekly review so it publishes the review just written rather\nthan last week's. The scheduled invocation is deliberately **not** `--dry-run`.\n\n**Three defects found by running it.** Worth recording because each was invisible until the\nchain was real:\n\n1. **`verify_dist` had no injectable clock.** Its freshness check fell through to\n `datetime.now(UTC)`, so its verdict depended on when it was called. That is what turned\n the suite red on **2026-08-09** with no code change: a test fixture manifest stamped with\n the absolute date `2026-07-26` silently aged past the 14-day limit, and the only failing\n assertion was a clock. `now` is now a parameter, threaded to `check_content`, and both\n directions are pinned by test. **`DEFAULT_MAX_AGE_DAYS` stays 14** \u2014 the limit was never\n wrong, only unmeasurable. Note this failure had **nothing to do with the stale deployed\n snapshot**, despite looking exactly like it would have: the fixture lives in `tmp_path`.\n2. **`npm` and `npx` could not be launched.** They are `.cmd` shims on Windows, which\n `CreateProcess` cannot execute by bare name under `shell=False`, so the first live run\n died with a `FileNotFoundError` before it could report or alert. The runner now resolves\n `argv[0]` through `shutil.which` \u2014 keeping `shell=False`, so the pinned argv stays exactly\n what was pinned \u2014 and an unlaunchable tool is a reportable abort rather than a traceback.\n3. **The chain poisoned its own next run.** Alerts are *published*: `glassbox.app` copies an\n alert `body` verbatim into `/api/timeline`. The chain report embeds the gate reports,\n which render the absolute directory they scanned, so run N's alert became run N+1's\n `windows_user_path` redaction \u2014 and under the amendment above a redaction aborts, so the\n chain would have deployed exactly once and then blocked forever. Observed precisely that\n way: the second live run aborted at `deploy` on `1 redaction in api-timeline.json`. Alert\n bodies are now relativized against `PROJECT_ROOT`. The two alert records already written\n by the runs that found this were **rewritten in place** to relativize their bodies \u2014\n records preserved, count preserved, backup left at\n `reports/alerts/alerts.jsonl.bak` \u2014 because otherwise the automation being enabled here\n could never have deployed again. Verified afterwards by capturing again *with* the deploy\n alert present: 0 redactions.\n\n**Live.** `quantlab glassbox refresh` deployed at 2026-08-10T22:55:34Z: 135 endpoints, 136\nfiles, 0 redactions, 160 published files scanned, 0 forbidden, 0 redactable \u2014 published to\nhttps://glassbox.danielmonzonautomation.com. The live manifest reads `generated_at\n2026-08-10T22:55:35Z` at commit `8a7f6ed`, against the 15.2-day-old `2026-07-26T18:02:16Z`\nit replaced. The previously-failing completeness test passes again." + }, + { + "date": "2026-08-10", + "title": "The share card could not spell \"quantlab\"", + "body": "**Finding.** `og-image.png` was drawn with a pixel font whose glyph set was incomplete. In a\nreal iMessage preview it read **\"Simu ated money on y.\"** and **\" uant ab \u00b7 autonomous\ntradin research\"** \u2014 every `l` and `q` dropped, every descender (`g`, `y`) clipped \u2014 and the\nwordmark **overlapped** the headline. For a site whose whole claim is that its figures can be\nchecked, a share card that cannot spell its own product name discredits it before a reader\narrives.\n\n**Decision.** Regenerated at 1200\u00d7630 from the **brand faces** in `docs/brand.md` \u2014 Fraunces\nVariable for display, Hanken Grotesk Variable for body \u2014 rendered by\n`frontend/scripts/og-image.mjs` through headless Chrome (`puppeteer-core` against the system\nbrowser, so no browser download). The fonts are the ones already vendored under\n`node_modules/@fontsource-variable` and are **inlined as base64**: the render touches no\nnetwork, and the faces carry complete Latin coverage, which is the actual fix. Colours are\nthe brand tokens verbatim, honey for the decorative fills and clay for warm-accent text, per\nthe honey/clay split in `docs/brand.md` \u00a71.\n\n**The generator verifies its own output.** A missing glyph is invisible to any DOM check \u2014\nthe text node is present whether or not the face can draw it. So the script measures each\ncharacter in the same font at the same size and requires a non-zero advance width *and* a\nnon-zero inked bounding box, requires `g y q p j` to actually descend below the baseline, and\nrequires that nothing overflows the card or collides with the rail. It exits non-zero and\nwrites no PNG if any check fails.\n\nThat last check earned its place immediately: the first regeneration fixed every glyph and\nintroduced a **new** overflow of the same class \u2014 \"AUTOMATION\" ran past the honey border and\nwas clipped mid-word. The checker caught it, the rail was widened to fit its longest line,\nand the assertion now stands guard. Every glyph was confirmed a second time by reading the\nfinished PNG back." + }, + { + "date": "2026-08-10", + "title": "Local scheduling is a reliability floor, and a VPS is the real fix", + "body": "**The exposure.** Every scheduled task \u2014 the four installed here plus the crypto run \u2014 fires\nfrom **Windows Task Scheduler on a single workstation**. `StartWhenAvailable` recovers a\nmissed start once the machine is awake, and that is genuinely useful: it is why several\ncatch-up runs exist in the record. What it cannot do is run anything while the machine is\n**off**. That is not hypothetical \u2014 it is the documented cause of the **2026-08-01** miss,\nwhere no crypto run fired at all, leaving a 70.40h mark interval, a 6.92h one, and a shadow\nsession with no paper counterpart. Divergence diagnosis #2 traced `crypto_voltarget`'s\nremaining above-threshold residual for week 2026-08-07 to exactly that gap.\n\nSo the failure mode is known, has already cost a data point, and will recur. Adding the Glass\nBox refresh to the same host extends the same exposure to publishing: a Friday with the\nmachine off means the site silently keeps last week's snapshot, and the only signal is the\nabsence of an email \u2014 the same shape of silent failure that let the site go fifteen days\nstale in the first place.\n\n**Decision: deferred to the post-day-90 review, deliberately.** Migrating the schedule to a\nVPS is the real fix, and it is not a scheduling change \u2014 it means relocating the `.env`\nsecrets, the parquet store, and the alert path to a host that is always on, which touches the\ntrading path's environment during a freeze and mid-track. The 90-day paper clock is measuring\nthis system *as configured*; changing where it runs partway through would fork the record it\nis accumulating. Not worth it to recover an occasional missed day.\n\n**Recorded so the day-90 review inherits it** rather than rediscovering it, alongside the\nturnover and 5 bps-spread questions from the same date. Until then the mitigation is what it\nalready is: `StartWhenAvailable` on every task, and a missed run being visible in the run\naudit rather than silent." + }, { "date": "2026-08-10", "title": "Divergence diagnosis #2, and six re-rulings", diff --git a/frontend/public/snapshot/api-divergence--label-crypto_trend.json b/frontend/public/snapshot/api-divergence--label-crypto_trend.json index 127c147..0393931 100644 --- a/frontend/public/snapshot/api-divergence--label-crypto_trend.json +++ b/frontend/public/snapshot/api-divergence--label-crypto_trend.json @@ -64,6 +64,21 @@ "window_start": "2026-07-30", "window_end": "2026-08-06", "structural_note": "No dividend drag applies here - crypto pays no dividends, so the adj_close shadow and the paper account see the same total return. The structural gap is MARK TIMING, and the 2026-07-24 diagnosis identified two mechanisms that dominate it. (1) Variable mark-window LENGTH: the shadow's sessions are uniform 24h UTC days, while consecutive paper marks are whatever the runs happened to land on - catch-up runs after a missed start produced windows from 10h to 33h in that week, so a paper 'daily' return can cover less than half or more than a third again of the day the shadow prices. (2) Mark-PHASE offset: a full 24h window struck at, say, 14:00 UTC still straddles two of the shadow's UTC days, so a trending stretch is split differently between them and both days show a gap. Weekend and overnight gaps are a secondary case of the same effect - a real but smaller contributor, since BTC trades 24/7 and no move is skipped, only attributed to a different mark. These gaps are largely self-cancelling in the WEEKLY aggregate the threshold is applied to, but individual days can swing well over 100 bps in either direction." + }, + { + "week_ending": "2026-08-14", + "label": "crypto_trend", + "asset_class": "crypto", + "paper_week_return": 0.0, + "shadow_week_return": 0.0, + "divergence_bps": 0.0, + "cumulative_divergence_bps": 0.0, + "verdict": "TRACKING", + "threshold_bps": 50.0, + "excluded_tail_days": [], + "window_start": "2026-08-08", + "window_end": "2026-08-14", + "structural_note": "No dividend drag applies here - crypto pays no dividends, so the adj_close shadow and the paper account see the same total return. The structural gap is MARK TIMING, and the 2026-07-24 diagnosis identified two mechanisms that dominate it. (1) Variable mark-window LENGTH: the shadow's sessions are uniform 24h UTC days, while consecutive paper marks are whatever the runs happened to land on - catch-up runs after a missed start produced windows from 10h to 33h in that week, so a paper 'daily' return can cover less than half or more than a third again of the day the shadow prices. (2) Mark-PHASE offset: a full 24h window struck at, say, 14:00 UTC still straddles two of the shadow's UTC days, so a trending stretch is split differently between them and both days show a gap. Weekend and overnight gaps are a secondary case of the same effect - a real but smaller contributor, since BTC trades 24/7 and no move is skipped, only attributed to a different mark. These gaps are largely self-cancelling in the WEEKLY aggregate the threshold is applied to, but individual days can swing well over 100 bps in either direction." } ], "corrections": [], diff --git a/frontend/public/snapshot/api-divergence--label-crypto_voltarget.json b/frontend/public/snapshot/api-divergence--label-crypto_voltarget.json index dae4dd6..361043b 100644 --- a/frontend/public/snapshot/api-divergence--label-crypto_voltarget.json +++ b/frontend/public/snapshot/api-divergence--label-crypto_voltarget.json @@ -64,6 +64,21 @@ "window_start": "2026-07-30", "window_end": "2026-08-06", "structural_note": "No dividend drag applies here - crypto pays no dividends, so the adj_close shadow and the paper account see the same total return. The structural gap is MARK TIMING, and the 2026-07-24 diagnosis identified two mechanisms that dominate it. (1) Variable mark-window LENGTH: the shadow's sessions are uniform 24h UTC days, while consecutive paper marks are whatever the runs happened to land on - catch-up runs after a missed start produced windows from 10h to 33h in that week, so a paper 'daily' return can cover less than half or more than a third again of the day the shadow prices. (2) Mark-PHASE offset: a full 24h window struck at, say, 14:00 UTC still straddles two of the shadow's UTC days, so a trending stretch is split differently between them and both days show a gap. Weekend and overnight gaps are a secondary case of the same effect - a real but smaller contributor, since BTC trades 24/7 and no move is skipped, only attributed to a different mark. These gaps are largely self-cancelling in the WEEKLY aggregate the threshold is applied to, but individual days can swing well over 100 bps in either direction." + }, + { + "week_ending": "2026-08-14", + "label": "crypto_voltarget", + "asset_class": "crypto", + "paper_week_return": -0.02134402756164666, + "shadow_week_return": -0.017924575978456203, + "divergence_bps": -34.19451583190458, + "cumulative_divergence_bps": -75.94120087669909, + "verdict": "TRACKING", + "threshold_bps": 50.0, + "excluded_tail_days": [], + "window_start": "2026-08-08", + "window_end": "2026-08-14", + "structural_note": "No dividend drag applies here - crypto pays no dividends, so the adj_close shadow and the paper account see the same total return. The structural gap is MARK TIMING, and the 2026-07-24 diagnosis identified two mechanisms that dominate it. (1) Variable mark-window LENGTH: the shadow's sessions are uniform 24h UTC days, while consecutive paper marks are whatever the runs happened to land on - catch-up runs after a missed start produced windows from 10h to 33h in that week, so a paper 'daily' return can cover less than half or more than a third again of the day the shadow prices. (2) Mark-PHASE offset: a full 24h window struck at, say, 14:00 UTC still straddles two of the shadow's UTC days, so a trending stretch is split differently between them and both days show a gap. Weekend and overnight gaps are a secondary case of the same effect - a real but smaller contributor, since BTC trades 24/7 and no move is skipped, only attributed to a different mark. These gaps are largely self-cancelling in the WEEKLY aggregate the threshold is applied to, but individual days can swing well over 100 bps in either direction." } ], "corrections": [ diff --git a/frontend/public/snapshot/api-divergence--label-trend.json b/frontend/public/snapshot/api-divergence--label-trend.json index 94d83ea..626b870 100644 --- a/frontend/public/snapshot/api-divergence--label-trend.json +++ b/frontend/public/snapshot/api-divergence--label-trend.json @@ -94,6 +94,23 @@ "window_start": "2026-07-31", "window_end": "2026-08-06", "structural_note": "Alpaca paper does not credit cash dividends while the shadow uses dividend-adjusted (adj_close) returns, so paper is EXPECTED to lag the shadow by roughly the portfolio's dividend yield over time. A negative cumulative divergence of that order is expected dividend drag, not tracking error." + }, + { + "week_ending": "2026-08-14", + "label": "trend", + "asset_class": "us_equity", + "paper_week_return": 0.008743931563651275, + "shadow_week_return": 0.005979951790851645, + "divergence_bps": 27.639797727996296, + "cumulative_divergence_bps": -4.710102464262622, + "verdict": "TRACKING", + "threshold_bps": 50.0, + "excluded_tail_days": [ + "2026-08-14" + ], + "window_start": "2026-08-07", + "window_end": "2026-08-13", + "structural_note": "Alpaca paper does not credit cash dividends while the shadow uses dividend-adjusted (adj_close) returns, so paper is EXPECTED to lag the shadow by roughly the portfolio's dividend yield over time. A negative cumulative divergence of that order is expected dividend drag, not tracking error." } ], "corrections": [ diff --git a/frontend/public/snapshot/api-divergence--label-voltarget.json b/frontend/public/snapshot/api-divergence--label-voltarget.json index 0559c37..77df2ba 100644 --- a/frontend/public/snapshot/api-divergence--label-voltarget.json +++ b/frontend/public/snapshot/api-divergence--label-voltarget.json @@ -94,6 +94,23 @@ "window_start": "2026-07-31", "window_end": "2026-08-06", "structural_note": "Alpaca paper does not credit cash dividends while the shadow uses dividend-adjusted (adj_close) returns, so paper is EXPECTED to lag the shadow by roughly the portfolio's dividend yield over time. A negative cumulative divergence of that order is expected dividend drag, not tracking error." + }, + { + "week_ending": "2026-08-14", + "label": "voltarget", + "asset_class": "us_equity", + "paper_week_return": 0.006048024676924335, + "shadow_week_return": 0.004318713054321188, + "divergence_bps": 17.29311622603147, + "cumulative_divergence_bps": 11.040929238457231, + "verdict": "TRACKING", + "threshold_bps": 50.0, + "excluded_tail_days": [ + "2026-08-14" + ], + "window_start": "2026-08-07", + "window_end": "2026-08-13", + "structural_note": "Alpaca paper does not credit cash dividends while the shadow uses dividend-adjusted (adj_close) returns, so paper is EXPECTED to lag the shadow by roughly the portfolio's dividend yield over time. A negative cumulative divergence of that order is expected dividend drag, not tracking error." } ], "corrections": [], diff --git a/frontend/public/snapshot/api-divergence.json b/frontend/public/snapshot/api-divergence.json index 0557904..ce8af87 100644 --- a/frontend/public/snapshot/api-divergence.json +++ b/frontend/public/snapshot/api-divergence.json @@ -316,6 +316,70 @@ "window_start": "2026-07-30", "window_end": "2026-08-06", "structural_note": "No dividend drag applies here - crypto pays no dividends, so the adj_close shadow and the paper account see the same total return. The structural gap is MARK TIMING, and the 2026-07-24 diagnosis identified two mechanisms that dominate it. (1) Variable mark-window LENGTH: the shadow's sessions are uniform 24h UTC days, while consecutive paper marks are whatever the runs happened to land on - catch-up runs after a missed start produced windows from 10h to 33h in that week, so a paper 'daily' return can cover less than half or more than a third again of the day the shadow prices. (2) Mark-PHASE offset: a full 24h window struck at, say, 14:00 UTC still straddles two of the shadow's UTC days, so a trending stretch is split differently between them and both days show a gap. Weekend and overnight gaps are a secondary case of the same effect - a real but smaller contributor, since BTC trades 24/7 and no move is skipped, only attributed to a different mark. These gaps are largely self-cancelling in the WEEKLY aggregate the threshold is applied to, but individual days can swing well over 100 bps in either direction." + }, + { + "week_ending": "2026-08-14", + "label": "voltarget", + "asset_class": "us_equity", + "paper_week_return": 0.006048024676924335, + "shadow_week_return": 0.004318713054321188, + "divergence_bps": 17.29311622603147, + "cumulative_divergence_bps": 11.040929238457231, + "verdict": "TRACKING", + "threshold_bps": 50.0, + "excluded_tail_days": [ + "2026-08-14" + ], + "window_start": "2026-08-07", + "window_end": "2026-08-13", + "structural_note": "Alpaca paper does not credit cash dividends while the shadow uses dividend-adjusted (adj_close) returns, so paper is EXPECTED to lag the shadow by roughly the portfolio's dividend yield over time. A negative cumulative divergence of that order is expected dividend drag, not tracking error." + }, + { + "week_ending": "2026-08-14", + "label": "trend", + "asset_class": "us_equity", + "paper_week_return": 0.008743931563651275, + "shadow_week_return": 0.005979951790851645, + "divergence_bps": 27.639797727996296, + "cumulative_divergence_bps": -4.710102464262622, + "verdict": "TRACKING", + "threshold_bps": 50.0, + "excluded_tail_days": [ + "2026-08-14" + ], + "window_start": "2026-08-07", + "window_end": "2026-08-13", + "structural_note": "Alpaca paper does not credit cash dividends while the shadow uses dividend-adjusted (adj_close) returns, so paper is EXPECTED to lag the shadow by roughly the portfolio's dividend yield over time. A negative cumulative divergence of that order is expected dividend drag, not tracking error." + }, + { + "week_ending": "2026-08-14", + "label": "crypto_trend", + "asset_class": "crypto", + "paper_week_return": 0.0, + "shadow_week_return": 0.0, + "divergence_bps": 0.0, + "cumulative_divergence_bps": 0.0, + "verdict": "TRACKING", + "threshold_bps": 50.0, + "excluded_tail_days": [], + "window_start": "2026-08-08", + "window_end": "2026-08-14", + "structural_note": "No dividend drag applies here - crypto pays no dividends, so the adj_close shadow and the paper account see the same total return. The structural gap is MARK TIMING, and the 2026-07-24 diagnosis identified two mechanisms that dominate it. (1) Variable mark-window LENGTH: the shadow's sessions are uniform 24h UTC days, while consecutive paper marks are whatever the runs happened to land on - catch-up runs after a missed start produced windows from 10h to 33h in that week, so a paper 'daily' return can cover less than half or more than a third again of the day the shadow prices. (2) Mark-PHASE offset: a full 24h window struck at, say, 14:00 UTC still straddles two of the shadow's UTC days, so a trending stretch is split differently between them and both days show a gap. Weekend and overnight gaps are a secondary case of the same effect - a real but smaller contributor, since BTC trades 24/7 and no move is skipped, only attributed to a different mark. These gaps are largely self-cancelling in the WEEKLY aggregate the threshold is applied to, but individual days can swing well over 100 bps in either direction." + }, + { + "week_ending": "2026-08-14", + "label": "crypto_voltarget", + "asset_class": "crypto", + "paper_week_return": -0.02134402756164666, + "shadow_week_return": -0.017924575978456203, + "divergence_bps": -34.19451583190458, + "cumulative_divergence_bps": -75.94120087669909, + "verdict": "TRACKING", + "threshold_bps": 50.0, + "excluded_tail_days": [], + "window_start": "2026-08-08", + "window_end": "2026-08-14", + "structural_note": "No dividend drag applies here - crypto pays no dividends, so the adj_close shadow and the paper account see the same total return. The structural gap is MARK TIMING, and the 2026-07-24 diagnosis identified two mechanisms that dominate it. (1) Variable mark-window LENGTH: the shadow's sessions are uniform 24h UTC days, while consecutive paper marks are whatever the runs happened to land on - catch-up runs after a missed start produced windows from 10h to 33h in that week, so a paper 'daily' return can cover less than half or more than a third again of the day the shadow prices. (2) Mark-PHASE offset: a full 24h window struck at, say, 14:00 UTC still straddles two of the shadow's UTC days, so a trending stretch is split differently between them and both days show a gap. Weekend and overnight gaps are a secondary case of the same effect - a real but smaller contributor, since BTC trades 24/7 and no move is skipped, only attributed to a different mark. These gaps are largely self-cancelling in the WEEKLY aggregate the threshold is applied to, but individual days can swing well over 100 bps in either direction." } ], "corrections": [ diff --git a/frontend/public/snapshot/api-equity--label-crypto_trend.json b/frontend/public/snapshot/api-equity--label-crypto_trend.json index 1439890..78040a0 100644 --- a/frontend/public/snapshot/api-equity--label-crypto_trend.json +++ b/frontend/public/snapshot/api-equity--label-crypto_trend.json @@ -220,12 +220,42 @@ "equity": 100000.0, "provenance": "on_schedule", "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." + }, + { + "timestamp": "2026-08-11T11:17:31.349349", + "equity": 100000.0, + "provenance": "catch_up", + "provenance_rationale": "Mark landed well outside the scheduled window \u2014 a StartWhenAvailable catch-up run after a missed start. The mark is real; its SPACING from the neighbouring marks is not one uniform session." + }, + { + "timestamp": "2026-08-12T12:37:19.836866", + "equity": 100000.0, + "provenance": "leaked", + "provenance_rationale": "A crypto mark produced by the 10:00 ET EQUITY task, which iterated the crypto accounts until the --asset-class us_equity fix on 2026-07-22." + }, + { + "timestamp": "2026-08-13T01:27:41.385485", + "equity": 100000.0, + "provenance": "catch_up", + "provenance_rationale": "Mark landed well outside the scheduled window \u2014 a StartWhenAvailable catch-up run after a missed start. The mark is real; its SPACING from the neighbouring marks is not one uniform session." + }, + { + "timestamp": "2026-08-14T06:38:10.490942", + "equity": 100000.0, + "provenance": "catch_up", + "provenance_rationale": "Mark landed well outside the scheduled window \u2014 a StartWhenAvailable catch-up run after a missed start. The mark is real; its SPACING from the neighbouring marks is not one uniform session." + }, + { + "timestamp": "2026-08-15T00:30:05.871787", + "equity": 100000.0, + "provenance": "on_schedule", + "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." } ], "provenance_counts": { - "catch_up": 15, - "leaked": 6, - "on_schedule": 15 + "catch_up": 18, + "leaked": 7, + "on_schedule": 16 } } ], diff --git a/frontend/public/snapshot/api-equity--label-crypto_voltarget.json b/frontend/public/snapshot/api-equity--label-crypto_voltarget.json index 223c923..0897067 100644 --- a/frontend/public/snapshot/api-equity--label-crypto_voltarget.json +++ b/frontend/public/snapshot/api-equity--label-crypto_voltarget.json @@ -220,12 +220,42 @@ "equity": 100798.2, "provenance": "on_schedule", "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." + }, + { + "timestamp": "2026-08-11T11:17:35.257478", + "equity": 100040.42, + "provenance": "catch_up", + "provenance_rationale": "Mark landed well outside the scheduled window \u2014 a StartWhenAvailable catch-up run after a missed start. The mark is real; its SPACING from the neighbouring marks is not one uniform session." + }, + { + "timestamp": "2026-08-12T12:37:22.272525", + "equity": 99810.63, + "provenance": "leaked", + "provenance_rationale": "A crypto mark produced by the 10:00 ET EQUITY task, which iterated the crypto accounts until the --asset-class us_equity fix on 2026-07-22." + }, + { + "timestamp": "2026-08-13T01:27:44.649765", + "equity": 98758.87, + "provenance": "catch_up", + "provenance_rationale": "Mark landed well outside the scheduled window \u2014 a StartWhenAvailable catch-up run after a missed start. The mark is real; its SPACING from the neighbouring marks is not one uniform session." + }, + { + "timestamp": "2026-08-14T06:38:13.328622", + "equity": 98535.82, + "provenance": "catch_up", + "provenance_rationale": "Mark landed well outside the scheduled window \u2014 a StartWhenAvailable catch-up run after a missed start. The mark is real; its SPACING from the neighbouring marks is not one uniform session." + }, + { + "timestamp": "2026-08-15T00:30:08.016859", + "equity": 98246.87, + "provenance": "on_schedule", + "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." } ], "provenance_counts": { - "catch_up": 15, - "leaked": 6, - "on_schedule": 15 + "catch_up": 18, + "leaked": 7, + "on_schedule": 16 } } ], diff --git a/frontend/public/snapshot/api-equity--label-trend.json b/frontend/public/snapshot/api-equity--label-trend.json index bf2f208..a8a2500 100644 --- a/frontend/public/snapshot/api-equity--label-trend.json +++ b/frontend/public/snapshot/api-equity--label-trend.json @@ -130,11 +130,35 @@ "equity": 102906.66, "provenance": "on_schedule", "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." + }, + { + "timestamp": "2026-08-11T14:00:12.753204", + "equity": 102905.33, + "provenance": "on_schedule", + "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." + }, + { + "timestamp": "2026-08-12T14:00:32.018986", + "equity": 102752.34, + "provenance": "on_schedule", + "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." + }, + { + "timestamp": "2026-08-13T14:00:07.974910", + "equity": 103437.44, + "provenance": "on_schedule", + "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." + }, + { + "timestamp": "2026-08-14T14:00:07.702473", + "equity": 103530.68, + "provenance": "on_schedule", + "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." } ], "provenance_counts": { "catch_up": 5, - "on_schedule": 16 + "on_schedule": 20 } } ], diff --git a/frontend/public/snapshot/api-equity--label-voltarget.json b/frontend/public/snapshot/api-equity--label-voltarget.json index d5211c7..d420ba7 100644 --- a/frontend/public/snapshot/api-equity--label-voltarget.json +++ b/frontend/public/snapshot/api-equity--label-voltarget.json @@ -136,11 +136,35 @@ "equity": 102712.19, "provenance": "on_schedule", "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." + }, + { + "timestamp": "2026-08-11T14:00:05.419719", + "equity": 102714.05, + "provenance": "on_schedule", + "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." + }, + { + "timestamp": "2026-08-12T14:00:25.624405", + "equity": 102594.59, + "provenance": "on_schedule", + "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." + }, + { + "timestamp": "2026-08-13T14:00:04.756492", + "equity": 103089.56, + "provenance": "on_schedule", + "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." + }, + { + "timestamp": "2026-08-14T14:00:04.460187", + "equity": 103171.09, + "provenance": "on_schedule", + "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." } ], "provenance_counts": { "catch_up": 6, - "on_schedule": 16 + "on_schedule": 20 } } ], diff --git a/frontend/public/snapshot/api-equity.json b/frontend/public/snapshot/api-equity.json index 8c9b2c5..318774e 100644 --- a/frontend/public/snapshot/api-equity.json +++ b/frontend/public/snapshot/api-equity.json @@ -136,11 +136,35 @@ "equity": 102712.19, "provenance": "on_schedule", "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." + }, + { + "timestamp": "2026-08-11T14:00:05.419719", + "equity": 102714.05, + "provenance": "on_schedule", + "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." + }, + { + "timestamp": "2026-08-12T14:00:25.624405", + "equity": 102594.59, + "provenance": "on_schedule", + "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." + }, + { + "timestamp": "2026-08-13T14:00:04.756492", + "equity": 103089.56, + "provenance": "on_schedule", + "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." + }, + { + "timestamp": "2026-08-14T14:00:04.460187", + "equity": 103171.09, + "provenance": "on_schedule", + "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." } ], "provenance_counts": { "catch_up": 6, - "on_schedule": 16 + "on_schedule": 20 } }, { @@ -272,11 +296,35 @@ "equity": 102906.66, "provenance": "on_schedule", "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." + }, + { + "timestamp": "2026-08-11T14:00:12.753204", + "equity": 102905.33, + "provenance": "on_schedule", + "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." + }, + { + "timestamp": "2026-08-12T14:00:32.018986", + "equity": 102752.34, + "provenance": "on_schedule", + "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." + }, + { + "timestamp": "2026-08-13T14:00:07.974910", + "equity": 103437.44, + "provenance": "on_schedule", + "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." + }, + { + "timestamp": "2026-08-14T14:00:07.702473", + "equity": 103530.68, + "provenance": "on_schedule", + "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." } ], "provenance_counts": { "catch_up": 5, - "on_schedule": 16 + "on_schedule": 20 } }, { @@ -498,12 +546,42 @@ "equity": 100000.0, "provenance": "on_schedule", "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." + }, + { + "timestamp": "2026-08-11T11:17:31.349349", + "equity": 100000.0, + "provenance": "catch_up", + "provenance_rationale": "Mark landed well outside the scheduled window \u2014 a StartWhenAvailable catch-up run after a missed start. The mark is real; its SPACING from the neighbouring marks is not one uniform session." + }, + { + "timestamp": "2026-08-12T12:37:19.836866", + "equity": 100000.0, + "provenance": "leaked", + "provenance_rationale": "A crypto mark produced by the 10:00 ET EQUITY task, which iterated the crypto accounts until the --asset-class us_equity fix on 2026-07-22." + }, + { + "timestamp": "2026-08-13T01:27:41.385485", + "equity": 100000.0, + "provenance": "catch_up", + "provenance_rationale": "Mark landed well outside the scheduled window \u2014 a StartWhenAvailable catch-up run after a missed start. The mark is real; its SPACING from the neighbouring marks is not one uniform session." + }, + { + "timestamp": "2026-08-14T06:38:10.490942", + "equity": 100000.0, + "provenance": "catch_up", + "provenance_rationale": "Mark landed well outside the scheduled window \u2014 a StartWhenAvailable catch-up run after a missed start. The mark is real; its SPACING from the neighbouring marks is not one uniform session." + }, + { + "timestamp": "2026-08-15T00:30:05.871787", + "equity": 100000.0, + "provenance": "on_schedule", + "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." } ], "provenance_counts": { - "catch_up": 15, - "leaked": 6, - "on_schedule": 15 + "catch_up": 18, + "leaked": 7, + "on_schedule": 16 } }, { @@ -725,12 +803,42 @@ "equity": 100798.2, "provenance": "on_schedule", "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." + }, + { + "timestamp": "2026-08-11T11:17:35.257478", + "equity": 100040.42, + "provenance": "catch_up", + "provenance_rationale": "Mark landed well outside the scheduled window \u2014 a StartWhenAvailable catch-up run after a missed start. The mark is real; its SPACING from the neighbouring marks is not one uniform session." + }, + { + "timestamp": "2026-08-12T12:37:22.272525", + "equity": 99810.63, + "provenance": "leaked", + "provenance_rationale": "A crypto mark produced by the 10:00 ET EQUITY task, which iterated the crypto accounts until the --asset-class us_equity fix on 2026-07-22." + }, + { + "timestamp": "2026-08-13T01:27:44.649765", + "equity": 98758.87, + "provenance": "catch_up", + "provenance_rationale": "Mark landed well outside the scheduled window \u2014 a StartWhenAvailable catch-up run after a missed start. The mark is real; its SPACING from the neighbouring marks is not one uniform session." + }, + { + "timestamp": "2026-08-14T06:38:13.328622", + "equity": 98535.82, + "provenance": "catch_up", + "provenance_rationale": "Mark landed well outside the scheduled window \u2014 a StartWhenAvailable catch-up run after a missed start. The mark is real; its SPACING from the neighbouring marks is not one uniform session." + }, + { + "timestamp": "2026-08-15T00:30:08.016859", + "equity": 98246.87, + "provenance": "on_schedule", + "provenance_rationale": "Mark landed within 30 minutes of this account's scheduled run time." } ], "provenance_counts": { - "catch_up": 15, - "leaked": 6, - "on_schedule": 15 + "catch_up": 18, + "leaked": 7, + "on_schedule": 16 } } ], diff --git a/frontend/public/snapshot/api-overview.json b/frontend/public/snapshot/api-overview.json index 9c2eb6d..9b2512a 100644 --- a/frontend/public/snapshot/api-overview.json +++ b/frontend/public/snapshot/api-overview.json @@ -1,13 +1,13 @@ { - "generated_at": "2026-08-10T22:58:11.073668Z", + "generated_at": "2026-08-15T16:57:39.301667Z", "accounts": [ { "label": "voltarget", "asset_class": "us_equity", - "latest_equity": 102712.19, - "latest_snapshot_at": "2026-08-10T14:00:06.395901", + "latest_equity": 103171.09, + "latest_snapshot_at": "2026-08-14T14:00:04.460187", "latest_snapshot_provenance": "on_schedule", - "snapshot_count": 22, + "snapshot_count": 26, "risk": { "halted": false, "reason": null, @@ -20,22 +20,20 @@ "clock": { "asset_class": "us_equity", "paper_start_date": "2026-07-09", - "calendar_days_elapsed": 29, + "calendar_days_elapsed": 36, "target_days": 90, - "pct_complete": 32.22222222222222, + "pct_complete": 40.0, "start_note": null, - "blockers": [ - "voltarget: DIVERGING week (+78 bps)" - ] + "blockers": [] } }, { "label": "trend", "asset_class": "us_equity", - "latest_equity": 102906.66, - "latest_snapshot_at": "2026-08-10T14:00:10.181589", + "latest_equity": 103530.68, + "latest_snapshot_at": "2026-08-14T14:00:07.702473", "latest_snapshot_provenance": "on_schedule", - "snapshot_count": 21, + "snapshot_count": 25, "risk": { "halted": false, "reason": null, @@ -48,22 +46,20 @@ "clock": { "asset_class": "us_equity", "paper_start_date": "2026-07-09", - "calendar_days_elapsed": 29, + "calendar_days_elapsed": 36, "target_days": 90, - "pct_complete": 32.22222222222222, + "pct_complete": 40.0, "start_note": null, - "blockers": [ - "trend: DIVERGING week (+101 bps)" - ] + "blockers": [] } }, { "label": "crypto_trend", "asset_class": "crypto", "latest_equity": 100000.0, - "latest_snapshot_at": "2026-08-10T00:30:07.345978", + "latest_snapshot_at": "2026-08-15T00:30:05.871787", "latest_snapshot_provenance": "on_schedule", - "snapshot_count": 36, + "snapshot_count": 41, "risk": { "halted": false, "reason": null, @@ -76,9 +72,9 @@ "clock": { "asset_class": "crypto", "paper_start_date": "2026-07-22", - "calendar_days_elapsed": 16, + "calendar_days_elapsed": 23, "target_days": 90, - "pct_complete": 17.77777777777778, + "pct_complete": 25.555555555555557, "start_note": "clock restarted 2026-07-22 by ruling; paper history back to 2026-07-12 is retained but does not count toward the gate", "blockers": [] } @@ -86,10 +82,10 @@ { "label": "crypto_voltarget", "asset_class": "crypto", - "latest_equity": 100798.2, - "latest_snapshot_at": "2026-08-10T00:30:09.667171", + "latest_equity": 98246.87, + "latest_snapshot_at": "2026-08-15T00:30:08.016859", "latest_snapshot_provenance": "on_schedule", - "snapshot_count": 36, + "snapshot_count": 41, "risk": { "halted": false, "reason": null, @@ -102,16 +98,14 @@ "clock": { "asset_class": "crypto", "paper_start_date": "2026-07-22", - "calendar_days_elapsed": 16, + "calendar_days_elapsed": 23, "target_days": 90, - "pct_complete": 17.77777777777778, + "pct_complete": 25.555555555555557, "start_note": "clock restarted 2026-07-22 by ruling; paper history back to 2026-07-12 is retained but does not count toward the gate", - "blockers": [ - "crypto_voltarget: DIVERGING week (+132 bps)" - ] + "blockers": [] } } ], - "week_ending": "2026-08-07", + "week_ending": "2026-08-14", "note": null } \ No newline at end of file diff --git a/frontend/public/snapshot/api-risk.json b/frontend/public/snapshot/api-risk.json index b3b9f0d..bd64c29 100644 --- a/frontend/public/snapshot/api-risk.json +++ b/frontend/public/snapshot/api-risk.json @@ -13,8 +13,8 @@ "weekly_divergence_alert_bps": 50.0 }, "limits_source": "risk.yaml", - "peak_equity": 102712.19, - "latest_equity": 102712.19, + "peak_equity": 103171.09, + "latest_equity": 103171.09, "current_drawdown": 0.0, "drawdown_kill_limit": 0.25, "drawdown_headroom": 0.25, @@ -39,8 +39,8 @@ "weekly_divergence_alert_bps": 50.0 }, "limits_source": "risk.yaml", - "peak_equity": 102906.66, - "latest_equity": 102906.66, + "peak_equity": 103530.68, + "latest_equity": 103530.68, "current_drawdown": 0.0, "drawdown_kill_limit": 0.25, "drawdown_headroom": 0.25, @@ -92,10 +92,10 @@ }, "limits_source": "crypto_risk.yaml", "peak_equity": 102465.73, - "latest_equity": 100798.2, - "current_drawdown": -0.01627402644767184, + "latest_equity": 98246.87, + "current_drawdown": -0.04117337572279045, "drawdown_kill_limit": 0.5, - "drawdown_headroom": 0.48372597355232816, + "drawdown_headroom": 0.45882662427720955, "kill_switch": { "halted": false, "reason": null, diff --git a/frontend/public/snapshot/api-runs--label-crypto_trend.json b/frontend/public/snapshot/api-runs--label-crypto_trend.json index a186236..5e8f7c4 100644 --- a/frontend/public/snapshot/api-runs--label-crypto_trend.json +++ b/frontend/public/snapshot/api-runs--label-crypto_trend.json @@ -1,7 +1,322 @@ { - "count": 36, + "count": 41, "label": "crypto_trend", "runs": [ + { + "run_id": "run_crypto_trend_20260815T003005Z", + "strategy": "crypto_trend", + "timestamp": "2026-08-15T00:30:05.871787Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 100000.0, + "target_weights": {}, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested BTC-USD" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated BTC-USD" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=100000.00 cash=100000.00" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-14 -> {}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=0.0)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_crypto_trend_20260814T063810Z", + "strategy": "crypto_trend", + "timestamp": "2026-08-14T06:38:10.490942Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 100000.0, + "target_weights": {}, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested BTC-USD" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated BTC-USD" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=100000.00 cash=100000.00" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-13 -> {}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=0.0)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_crypto_trend_20260813T012741Z", + "strategy": "crypto_trend", + "timestamp": "2026-08-13T01:27:41.385485Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 100000.0, + "target_weights": {}, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested BTC-USD" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated BTC-USD" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=100000.00 cash=100000.00" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-12 -> {}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=0.0)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_crypto_trend_20260812T123719Z", + "strategy": "crypto_trend", + "timestamp": "2026-08-12T12:37:19.836866Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 100000.0, + "target_weights": {}, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested BTC-USD" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated BTC-USD" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=100000.00 cash=100000.00" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-11 -> {}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=0.0)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_crypto_trend_20260811T111731Z", + "strategy": "crypto_trend", + "timestamp": "2026-08-11T11:17:31.349349Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 100000.0, + "target_weights": {}, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested BTC-USD" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated BTC-USD" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=100000.00 cash=100000.00" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-10 -> {}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=0.0)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, { "run_id": "run_crypto_trend_20260810T003007Z", "strategy": "crypto_trend", diff --git a/frontend/public/snapshot/api-runs--label-crypto_voltarget.json b/frontend/public/snapshot/api-runs--label-crypto_voltarget.json index 7e7775a..e0b2b7f 100644 --- a/frontend/public/snapshot/api-runs--label-crypto_voltarget.json +++ b/frontend/public/snapshot/api-runs--label-crypto_voltarget.json @@ -1,7 +1,401 @@ { - "count": 36, + "count": 41, "label": "crypto_voltarget", "runs": [ + { + "run_id": "run_crypto_voltarget_20260815T003008Z", + "strategy": "crypto_voltarget", + "timestamp": "2026-08-15T00:30:08.016859Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 98246.87, + "target_weights": { + "BTC-USD": 0.8897688975075773 + }, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested BTC-USD" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated BTC-USD" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=98246.87 cash=10511.63" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-14 -> {'BTC-USD': 0.8897688975075773}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=-0.04117337572279045)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_crypto_voltarget_20260814T063813Z", + "strategy": "crypto_voltarget", + "timestamp": "2026-08-14T06:38:13.328622Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 98535.82, + "target_weights": { + "BTC-USD": 0.8939608815069376 + }, + "no_trades": false, + "est_turnover": 0.032861717365410215, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested BTC-USD" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated BTC-USD" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=98535.82 cash=13686.71" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-13 -> {'BTC-USD': 0.8939608815069376}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=-0.038353408500578556)" + }, + { + "stage": "plan", + "ok": true, + "detail": "1 intent(s), buy_scale=1.0000, turnover=0.0329" + }, + { + "stage": "submit", + "ok": true, + "detail": "submitted 1 order(s), 0 duplicate(s)" + } + ], + "intents": [ + { + "symbol": "BTC-USD", + "side": "buy", + "notional": 3238.0562672089354, + "current_w": 0.8610991641415274, + "target_w": 0.8939608815069376 + } + ], + "submitted_orders": [ + { + "symbol": "BTC-USD", + "side": "buy", + "notional": 3238.06, + "status": "pending_new", + "client_order_id": "ql-crypto_voltarget-20260814-BTC-USD-buy", + "submitted_at": "2026-08-14T06:38:15.042685Z", + "was_duplicate": false + } + ] + }, + { + "run_id": "run_crypto_voltarget_20260813T012744Z", + "strategy": "crypto_voltarget", + "timestamp": "2026-08-13T01:27:44.649765Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 98758.87, + "target_weights": { + "BTC-USD": 0.8620658490902996 + }, + "no_trades": false, + "est_turnover": 0.03276930170169534, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested BTC-USD" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated BTC-USD" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=98758.87 cash=16858.48" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-12 -> {'BTC-USD': 0.8620658490902996}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=-0.03617658313662531)" + }, + { + "stage": "plan", + "ok": true, + "detail": "1 intent(s), buy_scale=1.0000, turnover=0.0328" + }, + { + "stage": "submit", + "ok": true, + "detail": "submitted 1 order(s), 0 duplicate(s)" + } + ], + "intents": [ + { + "symbol": "BTC-USD", + "side": "buy", + "notional": 3236.259206748509, + "current_w": 0.8292965473886043, + "target_w": 0.8620658490902996 + } + ], + "submitted_orders": [ + { + "symbol": "BTC-USD", + "side": "buy", + "notional": 3236.26, + "status": "pending_new", + "client_order_id": "ql-crypto_voltarget-20260813-BTC-USD-buy", + "submitted_at": "2026-08-13T01:27:46.057365Z", + "was_duplicate": false + } + ] + }, + { + "run_id": "run_crypto_voltarget_20260812T123722Z", + "strategy": "crypto_voltarget", + "timestamp": "2026-08-12T12:37:22.272525Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 99810.63, + "target_weights": { + "BTC-USD": 0.832964732069648 + }, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested BTC-USD" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated BTC-USD" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=99810.63 cash=16858.48" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-11 -> {'BTC-USD': 0.832964732069648}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=-0.025912078116263726)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_crypto_voltarget_20260811T111735Z", + "strategy": "crypto_voltarget", + "timestamp": "2026-08-11T11:17:35.257478Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 100040.42, + "target_weights": { + "BTC-USD": 0.8323567343776824 + }, + "no_trades": false, + "est_turnover": 0.044286670957316865, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested BTC-USD" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated BTC-USD" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=100040.42 cash=21201.56" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-10 -> {'BTC-USD': 0.8323567343776824}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=-0.023669474662406653)" + }, + { + "stage": "plan", + "ok": true, + "detail": "1 intent(s), buy_scale=1.0000, turnover=0.0443" + }, + { + "stage": "submit", + "ok": true, + "detail": "submitted 1 order(s), 0 duplicate(s)" + } + ], + "intents": [ + { + "symbol": "BTC-USD", + "side": "buy", + "notional": 4430.457162971781, + "current_w": 0.7880700634203656, + "target_w": 0.8323567343776824 + } + ], + "submitted_orders": [ + { + "symbol": "BTC-USD", + "side": "buy", + "notional": 4430.46, + "status": "pending_new", + "client_order_id": "ql-crypto_voltarget-20260811-BTC-USD-buy", + "submitted_at": "2026-08-11T11:17:34.415849Z", + "was_duplicate": false + } + ] + }, { "run_id": "run_crypto_voltarget_20260810T003009Z", "strategy": "crypto_voltarget", diff --git a/frontend/public/snapshot/api-runs--label-trend.json b/frontend/public/snapshot/api-runs--label-trend.json index d4bc661..ca71224 100644 --- a/frontend/public/snapshot/api-runs--label-trend.json +++ b/frontend/public/snapshot/api-runs--label-trend.json @@ -1,7 +1,267 @@ { - "count": 21, + "count": 25, "label": "trend", "runs": [ + { + "run_id": "run_trend_20260814T140007Z", + "strategy": "trend", + "timestamp": "2026-08-14T14:00:07.702473Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 103530.68, + "target_weights": { + "SPY": 1.0 + }, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested SPY, IEF" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated SPY, IEF" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=103530.68 cash=0.00" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-13 -> {'SPY': 1.0}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=0.0)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_trend_20260813T140007Z", + "strategy": "trend", + "timestamp": "2026-08-13T14:00:07.974910Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 103437.44, + "target_weights": { + "SPY": 1.0 + }, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested SPY, IEF" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated SPY, IEF" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=103437.44 cash=0.00" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-12 -> {'SPY': 1.0}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=0.0)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_trend_20260812T140032Z", + "strategy": "trend", + "timestamp": "2026-08-12T14:00:32.018986Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 102752.34, + "target_weights": { + "SPY": 1.0 + }, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested SPY, IEF" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated SPY, IEF" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=102752.34 cash=0.00" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-11 -> {'SPY': 1.0}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=-0.0014996113954141022)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_trend_20260811T140012Z", + "strategy": "trend", + "timestamp": "2026-08-11T14:00:12.753204Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 102905.33, + "target_weights": { + "SPY": 1.0 + }, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested SPY, IEF" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated SPY, IEF" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=102905.33 cash=0.00" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-10 -> {'SPY': 1.0}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=-1.2924333566033397e-05)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, { "run_id": "run_trend_20260810T140010Z", "strategy": "trend", diff --git a/frontend/public/snapshot/api-runs--label-voltarget.json b/frontend/public/snapshot/api-runs--label-voltarget.json index 9772585..f9e587a 100644 --- a/frontend/public/snapshot/api-runs--label-voltarget.json +++ b/frontend/public/snapshot/api-runs--label-voltarget.json @@ -1,7 +1,290 @@ { - "count": 22, + "count": 26, "label": "voltarget", "runs": [ + { + "run_id": "run_voltarget_20260814T140004Z", + "strategy": "voltarget", + "timestamp": "2026-08-14T14:00:04.460187Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 103171.09, + "target_weights": { + "SPY": 0.7157564384670008 + }, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested SPY" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated SPY" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=103171.09 cash=29364.89" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-13 -> {'SPY': 0.7157564384670008}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=0.0)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_voltarget_20260813T140004Z", + "strategy": "voltarget", + "timestamp": "2026-08-13T14:00:04.756492Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 103089.56, + "target_weights": { + "SPY": 0.7114196161850412 + }, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested SPY" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated SPY" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=103089.56 cash=29364.89" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-12 -> {'SPY': 0.7114196161850412}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=0.0)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_voltarget_20260812T140025Z", + "strategy": "voltarget", + "timestamp": "2026-08-12T14:00:25.624405Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 102594.59, + "target_weights": { + "SPY": 0.7100286644718531 + }, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested SPY" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated SPY" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=102594.59 cash=29364.89" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-11 -> {'SPY': 0.7100286644718531}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=-0.001163034657868156)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_voltarget_20260811T140005Z", + "strategy": "voltarget", + "timestamp": "2026-08-11T14:00:05.419719Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 102714.05, + "target_weights": { + "SPY": 0.7140967253638115 + }, + "no_trades": false, + "est_turnover": 0.014480924779568194, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested SPY" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated SPY" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=102714.05 cash=30852.28" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-10 -> {'SPY': 0.7140967253638115}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=0.0)" + }, + { + "stage": "plan", + "ok": true, + "detail": "1 intent(s), buy_scale=1.0000, turnover=0.0145" + }, + { + "stage": "submit", + "ok": true, + "detail": "submitted 1 order(s), 0 duplicate(s)" + } + ], + "intents": [ + { + "symbol": "SPY", + "side": "buy", + "notional": 1487.3944318548065, + "current_w": 0.6996158005842433, + "target_w": 0.7140967253638115 + } + ], + "submitted_orders": [ + { + "symbol": "SPY", + "side": "buy", + "notional": 1487.39, + "status": "pending_new", + "client_order_id": "ql-voltarget-20260811-SPY-buy", + "submitted_at": "2026-08-11T14:00:05.119384Z", + "was_duplicate": false + } + ] + }, { "run_id": "run_voltarget_20260810T140006Z", "strategy": "voltarget", diff --git a/frontend/public/snapshot/api-runs-run_crypto_trend_20260811T111731Z-narrate.json b/frontend/public/snapshot/api-runs-run_crypto_trend_20260811T111731Z-narrate.json new file mode 100644 index 0000000..e61d027 --- /dev/null +++ b/frontend/public/snapshot/api-runs-run_crypto_trend_20260811T111731Z-narrate.json @@ -0,0 +1,34 @@ +{ + "run_id": "run_crypto_trend_20260811T111731Z", + "strategy": "crypto_trend", + "narration": "Run run_crypto_trend_20260811T111731Z: account crypto_trend executed at 2026-08-11T11:17:31.349349Z as a LIVE SUBMIT against the paper broker. The gated pipeline completed 9 stages without aborting. Account equity was read as $100,000.00. Rule: crypto_trend holds the risk asset while its price is above its 10-month simple moving average, and rotates to the safe asset or cash otherwise. That rule produced a target of 100% cash (no positions). No orders were planned: every holding was already inside the minimum-trade band, so the run left the account untouched. Estimated turnover for the plan was 0.0000 of equity.", + "rule_sentences": [ + "Rule: crypto_trend holds the risk asset while its price is above its 10-month simple moving average, and rotates to the safe asset or cash otherwise." + ], + "counterfactuals": [], + "facts": [ + { + "rendered": "9", + "value": 9, + "source": "report.stages[*].ok" + }, + { + "rendered": "$100,000.00", + "value": 100000.0, + "source": "report.equity" + }, + { + "rendered": "10", + "value": 10, + "source": "rule_constant.crypto_trend.n_months" + }, + { + "rendered": "0.0000", + "value": 0.0, + "source": "report.plan.est_turnover" + } + ], + "disclaimer": "Generated from this run's structured fields and the strategy's pre-registered rule parameters only. It contains no market commentary, no news, and no inferred reasoning: every number above is traceable to a named source field.", + "available": true, + "note": null +} \ No newline at end of file diff --git a/frontend/public/snapshot/api-runs-run_crypto_trend_20260812T123719Z-narrate.json b/frontend/public/snapshot/api-runs-run_crypto_trend_20260812T123719Z-narrate.json new file mode 100644 index 0000000..8ff1e8c --- /dev/null +++ b/frontend/public/snapshot/api-runs-run_crypto_trend_20260812T123719Z-narrate.json @@ -0,0 +1,34 @@ +{ + "run_id": "run_crypto_trend_20260812T123719Z", + "strategy": "crypto_trend", + "narration": "Run run_crypto_trend_20260812T123719Z: account crypto_trend executed at 2026-08-12T12:37:19.836866Z as a LIVE SUBMIT against the paper broker. The gated pipeline completed 9 stages without aborting. Account equity was read as $100,000.00. Rule: crypto_trend holds the risk asset while its price is above its 10-month simple moving average, and rotates to the safe asset or cash otherwise. That rule produced a target of 100% cash (no positions). No orders were planned: every holding was already inside the minimum-trade band, so the run left the account untouched. Estimated turnover for the plan was 0.0000 of equity.", + "rule_sentences": [ + "Rule: crypto_trend holds the risk asset while its price is above its 10-month simple moving average, and rotates to the safe asset or cash otherwise." + ], + "counterfactuals": [], + "facts": [ + { + "rendered": "9", + "value": 9, + "source": "report.stages[*].ok" + }, + { + "rendered": "$100,000.00", + "value": 100000.0, + "source": "report.equity" + }, + { + "rendered": "10", + "value": 10, + "source": "rule_constant.crypto_trend.n_months" + }, + { + "rendered": "0.0000", + "value": 0.0, + "source": "report.plan.est_turnover" + } + ], + "disclaimer": "Generated from this run's structured fields and the strategy's pre-registered rule parameters only. It contains no market commentary, no news, and no inferred reasoning: every number above is traceable to a named source field.", + "available": true, + "note": null +} \ No newline at end of file diff --git a/frontend/public/snapshot/api-runs-run_crypto_trend_20260813T012741Z-narrate.json b/frontend/public/snapshot/api-runs-run_crypto_trend_20260813T012741Z-narrate.json new file mode 100644 index 0000000..2a36860 --- /dev/null +++ b/frontend/public/snapshot/api-runs-run_crypto_trend_20260813T012741Z-narrate.json @@ -0,0 +1,34 @@ +{ + "run_id": "run_crypto_trend_20260813T012741Z", + "strategy": "crypto_trend", + "narration": "Run run_crypto_trend_20260813T012741Z: account crypto_trend executed at 2026-08-13T01:27:41.385485Z as a LIVE SUBMIT against the paper broker. The gated pipeline completed 9 stages without aborting. Account equity was read as $100,000.00. Rule: crypto_trend holds the risk asset while its price is above its 10-month simple moving average, and rotates to the safe asset or cash otherwise. That rule produced a target of 100% cash (no positions). No orders were planned: every holding was already inside the minimum-trade band, so the run left the account untouched. Estimated turnover for the plan was 0.0000 of equity.", + "rule_sentences": [ + "Rule: crypto_trend holds the risk asset while its price is above its 10-month simple moving average, and rotates to the safe asset or cash otherwise." + ], + "counterfactuals": [], + "facts": [ + { + "rendered": "9", + "value": 9, + "source": "report.stages[*].ok" + }, + { + "rendered": "$100,000.00", + "value": 100000.0, + "source": "report.equity" + }, + { + "rendered": "10", + "value": 10, + "source": "rule_constant.crypto_trend.n_months" + }, + { + "rendered": "0.0000", + "value": 0.0, + "source": "report.plan.est_turnover" + } + ], + "disclaimer": "Generated from this run's structured fields and the strategy's pre-registered rule parameters only. It contains no market commentary, no news, and no inferred reasoning: every number above is traceable to a named source field.", + "available": true, + "note": null +} \ No newline at end of file diff --git a/frontend/public/snapshot/api-runs-run_crypto_trend_20260814T063810Z-narrate.json b/frontend/public/snapshot/api-runs-run_crypto_trend_20260814T063810Z-narrate.json new file mode 100644 index 0000000..0ba6557 --- /dev/null +++ b/frontend/public/snapshot/api-runs-run_crypto_trend_20260814T063810Z-narrate.json @@ -0,0 +1,34 @@ +{ + "run_id": "run_crypto_trend_20260814T063810Z", + "strategy": "crypto_trend", + "narration": "Run run_crypto_trend_20260814T063810Z: account crypto_trend executed at 2026-08-14T06:38:10.490942Z as a LIVE SUBMIT against the paper broker. The gated pipeline completed 9 stages without aborting. Account equity was read as $100,000.00. Rule: crypto_trend holds the risk asset while its price is above its 10-month simple moving average, and rotates to the safe asset or cash otherwise. That rule produced a target of 100% cash (no positions). No orders were planned: every holding was already inside the minimum-trade band, so the run left the account untouched. Estimated turnover for the plan was 0.0000 of equity.", + "rule_sentences": [ + "Rule: crypto_trend holds the risk asset while its price is above its 10-month simple moving average, and rotates to the safe asset or cash otherwise." + ], + "counterfactuals": [], + "facts": [ + { + "rendered": "9", + "value": 9, + "source": "report.stages[*].ok" + }, + { + "rendered": "$100,000.00", + "value": 100000.0, + "source": "report.equity" + }, + { + "rendered": "10", + "value": 10, + "source": "rule_constant.crypto_trend.n_months" + }, + { + "rendered": "0.0000", + "value": 0.0, + "source": "report.plan.est_turnover" + } + ], + "disclaimer": "Generated from this run's structured fields and the strategy's pre-registered rule parameters only. It contains no market commentary, no news, and no inferred reasoning: every number above is traceable to a named source field.", + "available": true, + "note": null +} \ No newline at end of file diff --git a/frontend/public/snapshot/api-runs-run_crypto_trend_20260815T003005Z-narrate.json b/frontend/public/snapshot/api-runs-run_crypto_trend_20260815T003005Z-narrate.json new file mode 100644 index 0000000..d3c8160 --- /dev/null +++ b/frontend/public/snapshot/api-runs-run_crypto_trend_20260815T003005Z-narrate.json @@ -0,0 +1,34 @@ +{ + "run_id": "run_crypto_trend_20260815T003005Z", + "strategy": "crypto_trend", + "narration": "Run run_crypto_trend_20260815T003005Z: account crypto_trend executed at 2026-08-15T00:30:05.871787Z as a LIVE SUBMIT against the paper broker. The gated pipeline completed 9 stages without aborting. Account equity was read as $100,000.00. Rule: crypto_trend holds the risk asset while its price is above its 10-month simple moving average, and rotates to the safe asset or cash otherwise. That rule produced a target of 100% cash (no positions). No orders were planned: every holding was already inside the minimum-trade band, so the run left the account untouched. Estimated turnover for the plan was 0.0000 of equity.", + "rule_sentences": [ + "Rule: crypto_trend holds the risk asset while its price is above its 10-month simple moving average, and rotates to the safe asset or cash otherwise." + ], + "counterfactuals": [], + "facts": [ + { + "rendered": "9", + "value": 9, + "source": "report.stages[*].ok" + }, + { + "rendered": "$100,000.00", + "value": 100000.0, + "source": "report.equity" + }, + { + "rendered": "10", + "value": 10, + "source": "rule_constant.crypto_trend.n_months" + }, + { + "rendered": "0.0000", + "value": 0.0, + "source": "report.plan.est_turnover" + } + ], + "disclaimer": "Generated from this run's structured fields and the strategy's pre-registered rule parameters only. It contains no market commentary, no news, and no inferred reasoning: every number above is traceable to a named source field.", + "available": true, + "note": null +} \ No newline at end of file diff --git a/frontend/public/snapshot/api-runs-run_crypto_voltarget_20260811T111735Z-narrate.json b/frontend/public/snapshot/api-runs-run_crypto_voltarget_20260811T111735Z-narrate.json new file mode 100644 index 0000000..fd8753d --- /dev/null +++ b/frontend/public/snapshot/api-runs-run_crypto_voltarget_20260811T111735Z-narrate.json @@ -0,0 +1,86 @@ +{ + "run_id": "run_crypto_voltarget_20260811T111735Z", + "strategy": "crypto_voltarget", + "narration": "Run run_crypto_voltarget_20260811T111735Z: account crypto_voltarget executed at 2026-08-11T11:17:35.257478Z as a LIVE SUBMIT against the paper broker. The gated pipeline completed 10 stages without aborting. Account equity was read as $100,040.42. Rule: crypto_voltarget sizes exposure as its target volatility (20%) divided by trailing 20-day realized volatility, annualized on a 365-day year, capped at 100% of equity; the remainder is held in cash. That rule produced a target of BTC-USD at 83.24%. Planned orders: BUY BTC-USD for $4,430.46 notional moving weight from 78.81% to 83.24%. Estimated turnover for the plan was 0.0443 of equity. Submitted to the broker: buy BTC-USD for $4,430.46 \u2014 broker status 'pending_new'. Branches the rule did not take: BTC-USD: traded because drift was 4.43%, above the 1.00% minimum-trade band; had drift been at or below that band the runner would have left the position to drift untouched, placing no order.", + "rule_sentences": [ + "Rule: crypto_voltarget sizes exposure as its target volatility (20%) divided by trailing 20-day realized volatility, annualized on a 365-day year, capped at 100% of equity; the remainder is held in cash." + ], + "counterfactuals": [ + "BTC-USD: traded because drift was 4.43%, above the 1.00% minimum-trade band; had drift been at or below that band the runner would have left the position to drift untouched, placing no order." + ], + "facts": [ + { + "rendered": "10", + "value": 10, + "source": "report.stages[*].ok" + }, + { + "rendered": "$100,040.42", + "value": 100040.42, + "source": "report.equity" + }, + { + "rendered": "20%", + "value": 0.2, + "source": "rule_constant.crypto_voltarget.target_vol" + }, + { + "rendered": "20", + "value": 20, + "source": "rule_constant.crypto_voltarget.lookback_days" + }, + { + "rendered": "365", + "value": 365, + "source": "rule_constant.crypto_voltarget.periods_per_year" + }, + { + "rendered": "100%", + "value": 1.0, + "source": "rule_constant.crypto_voltarget.max_weight" + }, + { + "rendered": "83.24%", + "value": 0.8323567343776824, + "source": "report.target_weights.BTC-USD" + }, + { + "rendered": "$4,430.46", + "value": 4430.457162971781, + "source": "report.plan.intents[0].notional" + }, + { + "rendered": "78.81%", + "value": 0.7880700634203656, + "source": "report.plan.intents[0].current_w" + }, + { + "rendered": "83.24%", + "value": 0.8323567343776824, + "source": "report.plan.intents[0].target_w" + }, + { + "rendered": "4.43%", + "value": 0.044286670957316865, + "source": "derived.abs(report.plan.intents[0].target_w - report.plan.intents[0].current_w)" + }, + { + "rendered": "1.00%", + "value": 0.01, + "source": "report.plan.min_trade_frac" + }, + { + "rendered": "0.0443", + "value": 0.044286670957316865, + "source": "report.plan.est_turnover" + }, + { + "rendered": "$4,430.46", + "value": 4430.46, + "source": "report.submitted_orders[0].notional" + } + ], + "disclaimer": "Generated from this run's structured fields and the strategy's pre-registered rule parameters only. It contains no market commentary, no news, and no inferred reasoning: every number above is traceable to a named source field.", + "available": true, + "note": null +} \ No newline at end of file diff --git a/frontend/public/snapshot/api-runs-run_crypto_voltarget_20260812T123722Z-narrate.json b/frontend/public/snapshot/api-runs-run_crypto_voltarget_20260812T123722Z-narrate.json new file mode 100644 index 0000000..a05bd27 --- /dev/null +++ b/frontend/public/snapshot/api-runs-run_crypto_voltarget_20260812T123722Z-narrate.json @@ -0,0 +1,66 @@ +{ + "run_id": "run_crypto_voltarget_20260812T123722Z", + "strategy": "crypto_voltarget", + "narration": "Run run_crypto_voltarget_20260812T123722Z: account crypto_voltarget executed at 2026-08-12T12:37:22.272525Z as a LIVE SUBMIT against the paper broker. The gated pipeline completed 9 stages without aborting. Account equity was read as $99,810.63. Rule: crypto_voltarget sizes exposure as its target volatility (20%) divided by trailing 20-day realized volatility, annualized on a 365-day year, capped at 100% of equity; the remainder is held in cash. That rule produced a target of BTC-USD at 83.30%. No orders were planned: every holding was already inside the minimum-trade band, so the run left the account untouched. Estimated turnover for the plan was 0.0000 of equity. Branches the rule did not take: BTC-USD: NOT traded because drift was 0.19%, at or below the 1.00% minimum-trade band; had drift exceeded that band the runner would have re-traded it back to target.", + "rule_sentences": [ + "Rule: crypto_voltarget sizes exposure as its target volatility (20%) divided by trailing 20-day realized volatility, annualized on a 365-day year, capped at 100% of equity; the remainder is held in cash." + ], + "counterfactuals": [ + "BTC-USD: NOT traded because drift was 0.19%, at or below the 1.00% minimum-trade band; had drift exceeded that band the runner would have re-traded it back to target." + ], + "facts": [ + { + "rendered": "9", + "value": 9, + "source": "report.stages[*].ok" + }, + { + "rendered": "$99,810.63", + "value": 99810.63, + "source": "report.equity" + }, + { + "rendered": "20%", + "value": 0.2, + "source": "rule_constant.crypto_voltarget.target_vol" + }, + { + "rendered": "20", + "value": 20, + "source": "rule_constant.crypto_voltarget.lookback_days" + }, + { + "rendered": "365", + "value": 365, + "source": "rule_constant.crypto_voltarget.periods_per_year" + }, + { + "rendered": "100%", + "value": 1.0, + "source": "rule_constant.crypto_voltarget.max_weight" + }, + { + "rendered": "83.30%", + "value": 0.832964732069648, + "source": "report.target_weights.BTC-USD" + }, + { + "rendered": "0.19%", + "value": 0.0018693867742622938, + "source": "derived.abs(report.plan.skipped[0].diff)" + }, + { + "rendered": "1.00%", + "value": 0.01, + "source": "report.plan.min_trade_frac" + }, + { + "rendered": "0.0000", + "value": 0.0, + "source": "report.plan.est_turnover" + } + ], + "disclaimer": "Generated from this run's structured fields and the strategy's pre-registered rule parameters only. It contains no market commentary, no news, and no inferred reasoning: every number above is traceable to a named source field.", + "available": true, + "note": null +} \ No newline at end of file diff --git a/frontend/public/snapshot/api-runs-run_crypto_voltarget_20260813T012744Z-narrate.json b/frontend/public/snapshot/api-runs-run_crypto_voltarget_20260813T012744Z-narrate.json new file mode 100644 index 0000000..a8845c8 --- /dev/null +++ b/frontend/public/snapshot/api-runs-run_crypto_voltarget_20260813T012744Z-narrate.json @@ -0,0 +1,86 @@ +{ + "run_id": "run_crypto_voltarget_20260813T012744Z", + "strategy": "crypto_voltarget", + "narration": "Run run_crypto_voltarget_20260813T012744Z: account crypto_voltarget executed at 2026-08-13T01:27:44.649765Z as a LIVE SUBMIT against the paper broker. The gated pipeline completed 10 stages without aborting. Account equity was read as $98,758.87. Rule: crypto_voltarget sizes exposure as its target volatility (20%) divided by trailing 20-day realized volatility, annualized on a 365-day year, capped at 100% of equity; the remainder is held in cash. That rule produced a target of BTC-USD at 86.21%. Planned orders: BUY BTC-USD for $3,236.26 notional moving weight from 82.93% to 86.21%. Estimated turnover for the plan was 0.0328 of equity. Submitted to the broker: buy BTC-USD for $3,236.26 \u2014 broker status 'pending_new'. Branches the rule did not take: BTC-USD: traded because drift was 3.28%, above the 1.00% minimum-trade band; had drift been at or below that band the runner would have left the position to drift untouched, placing no order.", + "rule_sentences": [ + "Rule: crypto_voltarget sizes exposure as its target volatility (20%) divided by trailing 20-day realized volatility, annualized on a 365-day year, capped at 100% of equity; the remainder is held in cash." + ], + "counterfactuals": [ + "BTC-USD: traded because drift was 3.28%, above the 1.00% minimum-trade band; had drift been at or below that band the runner would have left the position to drift untouched, placing no order." + ], + "facts": [ + { + "rendered": "10", + "value": 10, + "source": "report.stages[*].ok" + }, + { + "rendered": "$98,758.87", + "value": 98758.87, + "source": "report.equity" + }, + { + "rendered": "20%", + "value": 0.2, + "source": "rule_constant.crypto_voltarget.target_vol" + }, + { + "rendered": "20", + "value": 20, + "source": "rule_constant.crypto_voltarget.lookback_days" + }, + { + "rendered": "365", + "value": 365, + "source": "rule_constant.crypto_voltarget.periods_per_year" + }, + { + "rendered": "100%", + "value": 1.0, + "source": "rule_constant.crypto_voltarget.max_weight" + }, + { + "rendered": "86.21%", + "value": 0.8620658490902996, + "source": "report.target_weights.BTC-USD" + }, + { + "rendered": "$3,236.26", + "value": 3236.259206748509, + "source": "report.plan.intents[0].notional" + }, + { + "rendered": "82.93%", + "value": 0.8292965473886043, + "source": "report.plan.intents[0].current_w" + }, + { + "rendered": "86.21%", + "value": 0.8620658490902996, + "source": "report.plan.intents[0].target_w" + }, + { + "rendered": "3.28%", + "value": 0.03276930170169534, + "source": "derived.abs(report.plan.intents[0].target_w - report.plan.intents[0].current_w)" + }, + { + "rendered": "1.00%", + "value": 0.01, + "source": "report.plan.min_trade_frac" + }, + { + "rendered": "0.0328", + "value": 0.03276930170169534, + "source": "report.plan.est_turnover" + }, + { + "rendered": "$3,236.26", + "value": 3236.26, + "source": "report.submitted_orders[0].notional" + } + ], + "disclaimer": "Generated from this run's structured fields and the strategy's pre-registered rule parameters only. It contains no market commentary, no news, and no inferred reasoning: every number above is traceable to a named source field.", + "available": true, + "note": null +} \ No newline at end of file diff --git a/frontend/public/snapshot/api-runs-run_crypto_voltarget_20260814T063813Z-narrate.json b/frontend/public/snapshot/api-runs-run_crypto_voltarget_20260814T063813Z-narrate.json new file mode 100644 index 0000000..f0bcdae --- /dev/null +++ b/frontend/public/snapshot/api-runs-run_crypto_voltarget_20260814T063813Z-narrate.json @@ -0,0 +1,86 @@ +{ + "run_id": "run_crypto_voltarget_20260814T063813Z", + "strategy": "crypto_voltarget", + "narration": "Run run_crypto_voltarget_20260814T063813Z: account crypto_voltarget executed at 2026-08-14T06:38:13.328622Z as a LIVE SUBMIT against the paper broker. The gated pipeline completed 10 stages without aborting. Account equity was read as $98,535.82. Rule: crypto_voltarget sizes exposure as its target volatility (20%) divided by trailing 20-day realized volatility, annualized on a 365-day year, capped at 100% of equity; the remainder is held in cash. That rule produced a target of BTC-USD at 89.40%. Planned orders: BUY BTC-USD for $3,238.06 notional moving weight from 86.11% to 89.40%. Estimated turnover for the plan was 0.0329 of equity. Submitted to the broker: buy BTC-USD for $3,238.06 \u2014 broker status 'pending_new'. Branches the rule did not take: BTC-USD: traded because drift was 3.29%, above the 1.00% minimum-trade band; had drift been at or below that band the runner would have left the position to drift untouched, placing no order.", + "rule_sentences": [ + "Rule: crypto_voltarget sizes exposure as its target volatility (20%) divided by trailing 20-day realized volatility, annualized on a 365-day year, capped at 100% of equity; the remainder is held in cash." + ], + "counterfactuals": [ + "BTC-USD: traded because drift was 3.29%, above the 1.00% minimum-trade band; had drift been at or below that band the runner would have left the position to drift untouched, placing no order." + ], + "facts": [ + { + "rendered": "10", + "value": 10, + "source": "report.stages[*].ok" + }, + { + "rendered": "$98,535.82", + "value": 98535.82, + "source": "report.equity" + }, + { + "rendered": "20%", + "value": 0.2, + "source": "rule_constant.crypto_voltarget.target_vol" + }, + { + "rendered": "20", + "value": 20, + "source": "rule_constant.crypto_voltarget.lookback_days" + }, + { + "rendered": "365", + "value": 365, + "source": "rule_constant.crypto_voltarget.periods_per_year" + }, + { + "rendered": "100%", + "value": 1.0, + "source": "rule_constant.crypto_voltarget.max_weight" + }, + { + "rendered": "89.40%", + "value": 0.8939608815069376, + "source": "report.target_weights.BTC-USD" + }, + { + "rendered": "$3,238.06", + "value": 3238.0562672089354, + "source": "report.plan.intents[0].notional" + }, + { + "rendered": "86.11%", + "value": 0.8610991641415274, + "source": "report.plan.intents[0].current_w" + }, + { + "rendered": "89.40%", + "value": 0.8939608815069376, + "source": "report.plan.intents[0].target_w" + }, + { + "rendered": "3.29%", + "value": 0.032861717365410215, + "source": "derived.abs(report.plan.intents[0].target_w - report.plan.intents[0].current_w)" + }, + { + "rendered": "1.00%", + "value": 0.01, + "source": "report.plan.min_trade_frac" + }, + { + "rendered": "0.0329", + "value": 0.032861717365410215, + "source": "report.plan.est_turnover" + }, + { + "rendered": "$3,238.06", + "value": 3238.06, + "source": "report.submitted_orders[0].notional" + } + ], + "disclaimer": "Generated from this run's structured fields and the strategy's pre-registered rule parameters only. It contains no market commentary, no news, and no inferred reasoning: every number above is traceable to a named source field.", + "available": true, + "note": null +} \ No newline at end of file diff --git a/frontend/public/snapshot/api-runs-run_crypto_voltarget_20260815T003008Z-narrate.json b/frontend/public/snapshot/api-runs-run_crypto_voltarget_20260815T003008Z-narrate.json new file mode 100644 index 0000000..0d43f72 --- /dev/null +++ b/frontend/public/snapshot/api-runs-run_crypto_voltarget_20260815T003008Z-narrate.json @@ -0,0 +1,66 @@ +{ + "run_id": "run_crypto_voltarget_20260815T003008Z", + "strategy": "crypto_voltarget", + "narration": "Run run_crypto_voltarget_20260815T003008Z: account crypto_voltarget executed at 2026-08-15T00:30:08.016859Z as a LIVE SUBMIT against the paper broker. The gated pipeline completed 9 stages without aborting. Account equity was read as $98,246.87. Rule: crypto_voltarget sizes exposure as its target volatility (20%) divided by trailing 20-day realized volatility, annualized on a 365-day year, capped at 100% of equity; the remainder is held in cash. That rule produced a target of BTC-USD at 88.98%. No orders were planned: every holding was already inside the minimum-trade band, so the run left the account untouched. Estimated turnover for the plan was 0.0000 of equity. Branches the rule did not take: BTC-USD: NOT traded because drift was 0.32%, at or below the 1.00% minimum-trade band; had drift exceeded that band the runner would have re-traded it back to target.", + "rule_sentences": [ + "Rule: crypto_voltarget sizes exposure as its target volatility (20%) divided by trailing 20-day realized volatility, annualized on a 365-day year, capped at 100% of equity; the remainder is held in cash." + ], + "counterfactuals": [ + "BTC-USD: NOT traded because drift was 0.32%, at or below the 1.00% minimum-trade band; had drift exceeded that band the runner would have re-traded it back to target." + ], + "facts": [ + { + "rendered": "9", + "value": 9, + "source": "report.stages[*].ok" + }, + { + "rendered": "$98,246.87", + "value": 98246.87, + "source": "report.equity" + }, + { + "rendered": "20%", + "value": 0.2, + "source": "rule_constant.crypto_voltarget.target_vol" + }, + { + "rendered": "20", + "value": 20, + "source": "rule_constant.crypto_voltarget.lookback_days" + }, + { + "rendered": "365", + "value": 365, + "source": "rule_constant.crypto_voltarget.periods_per_year" + }, + { + "rendered": "100%", + "value": 1.0, + "source": "rule_constant.crypto_voltarget.max_weight" + }, + { + "rendered": "88.98%", + "value": 0.8897688975075773, + "source": "report.target_weights.BTC-USD" + }, + { + "rendered": "0.32%", + "value": 0.003239139684854564, + "source": "derived.abs(report.plan.skipped[0].diff)" + }, + { + "rendered": "1.00%", + "value": 0.01, + "source": "report.plan.min_trade_frac" + }, + { + "rendered": "0.0000", + "value": 0.0, + "source": "report.plan.est_turnover" + } + ], + "disclaimer": "Generated from this run's structured fields and the strategy's pre-registered rule parameters only. It contains no market commentary, no news, and no inferred reasoning: every number above is traceable to a named source field.", + "available": true, + "note": null +} \ No newline at end of file diff --git a/frontend/public/snapshot/api-runs-run_trend_20260811T140012Z-narrate.json b/frontend/public/snapshot/api-runs-run_trend_20260811T140012Z-narrate.json new file mode 100644 index 0000000..3515563 --- /dev/null +++ b/frontend/public/snapshot/api-runs-run_trend_20260811T140012Z-narrate.json @@ -0,0 +1,51 @@ +{ + "run_id": "run_trend_20260811T140012Z", + "strategy": "trend", + "narration": "Run run_trend_20260811T140012Z: account trend executed at 2026-08-11T14:00:12.753204Z as a LIVE SUBMIT against the paper broker. The gated pipeline completed 9 stages without aborting. Account equity was read as $102,905.33. Rule: trend holds the risk asset while its price is above its 10-month simple moving average, and rotates to the safe asset or cash otherwise. That rule produced a target of SPY at 100.00%. No orders were planned: every holding was already inside the minimum-trade band, so the run left the account untouched. Estimated turnover for the plan was 0.0000 of equity. Branches the rule did not take: SPY: NOT traded because drift was 0.00%, at or below the 1.00% minimum-trade band; had drift exceeded that band the runner would have re-traded it back to target.", + "rule_sentences": [ + "Rule: trend holds the risk asset while its price is above its 10-month simple moving average, and rotates to the safe asset or cash otherwise." + ], + "counterfactuals": [ + "SPY: NOT traded because drift was 0.00%, at or below the 1.00% minimum-trade band; had drift exceeded that band the runner would have re-traded it back to target." + ], + "facts": [ + { + "rendered": "9", + "value": 9, + "source": "report.stages[*].ok" + }, + { + "rendered": "$102,905.33", + "value": 102905.33, + "source": "report.equity" + }, + { + "rendered": "10", + "value": 10, + "source": "rule_constant.trend.n_months" + }, + { + "rendered": "100.00%", + "value": 1.0, + "source": "report.target_weights.SPY" + }, + { + "rendered": "0.00%", + "value": 3.1067389771877174e-08, + "source": "derived.abs(report.plan.skipped[0].diff)" + }, + { + "rendered": "1.00%", + "value": 0.01, + "source": "report.plan.min_trade_frac" + }, + { + "rendered": "0.0000", + "value": 0.0, + "source": "report.plan.est_turnover" + } + ], + "disclaimer": "Generated from this run's structured fields and the strategy's pre-registered rule parameters only. It contains no market commentary, no news, and no inferred reasoning: every number above is traceable to a named source field.", + "available": true, + "note": null +} \ No newline at end of file diff --git a/frontend/public/snapshot/api-runs-run_trend_20260812T140032Z-narrate.json b/frontend/public/snapshot/api-runs-run_trend_20260812T140032Z-narrate.json new file mode 100644 index 0000000..3436591 --- /dev/null +++ b/frontend/public/snapshot/api-runs-run_trend_20260812T140032Z-narrate.json @@ -0,0 +1,51 @@ +{ + "run_id": "run_trend_20260812T140032Z", + "strategy": "trend", + "narration": "Run run_trend_20260812T140032Z: account trend executed at 2026-08-12T14:00:32.018986Z as a LIVE SUBMIT against the paper broker. The gated pipeline completed 9 stages without aborting. Account equity was read as $102,752.34. Rule: trend holds the risk asset while its price is above its 10-month simple moving average, and rotates to the safe asset or cash otherwise. That rule produced a target of SPY at 100.00%. No orders were planned: every holding was already inside the minimum-trade band, so the run left the account untouched. Estimated turnover for the plan was 0.0000 of equity. Branches the rule did not take: SPY: NOT traded because drift was 0.00%, at or below the 1.00% minimum-trade band; had drift exceeded that band the runner would have re-traded it back to target.", + "rule_sentences": [ + "Rule: trend holds the risk asset while its price is above its 10-month simple moving average, and rotates to the safe asset or cash otherwise." + ], + "counterfactuals": [ + "SPY: NOT traded because drift was 0.00%, at or below the 1.00% minimum-trade band; had drift exceeded that band the runner would have re-traded it back to target." + ], + "facts": [ + { + "rendered": "9", + "value": 9, + "source": "report.stages[*].ok" + }, + { + "rendered": "$102,752.34", + "value": 102752.34, + "source": "report.equity" + }, + { + "rendered": "10", + "value": 10, + "source": "rule_constant.trend.n_months" + }, + { + "rendered": "100.00%", + "value": 1.0, + "source": "report.target_weights.SPY" + }, + { + "rendered": "0.00%", + "value": 4.208176673792252e-08, + "source": "derived.abs(report.plan.skipped[0].diff)" + }, + { + "rendered": "1.00%", + "value": 0.01, + "source": "report.plan.min_trade_frac" + }, + { + "rendered": "0.0000", + "value": 0.0, + "source": "report.plan.est_turnover" + } + ], + "disclaimer": "Generated from this run's structured fields and the strategy's pre-registered rule parameters only. It contains no market commentary, no news, and no inferred reasoning: every number above is traceable to a named source field.", + "available": true, + "note": null +} \ No newline at end of file diff --git a/frontend/public/snapshot/api-runs-run_trend_20260813T140007Z-narrate.json b/frontend/public/snapshot/api-runs-run_trend_20260813T140007Z-narrate.json new file mode 100644 index 0000000..47601db --- /dev/null +++ b/frontend/public/snapshot/api-runs-run_trend_20260813T140007Z-narrate.json @@ -0,0 +1,51 @@ +{ + "run_id": "run_trend_20260813T140007Z", + "strategy": "trend", + "narration": "Run run_trend_20260813T140007Z: account trend executed at 2026-08-13T14:00:07.974910Z as a LIVE SUBMIT against the paper broker. The gated pipeline completed 9 stages without aborting. Account equity was read as $103,437.44. Rule: trend holds the risk asset while its price is above its 10-month simple moving average, and rotates to the safe asset or cash otherwise. That rule produced a target of SPY at 100.00%. No orders were planned: every holding was already inside the minimum-trade band, so the run left the account untouched. Estimated turnover for the plan was 0.0000 of equity. Branches the rule did not take: SPY: NOT traded because drift was 0.00%, at or below the 1.00% minimum-trade band; had drift exceeded that band the runner would have re-traded it back to target.", + "rule_sentences": [ + "Rule: trend holds the risk asset while its price is above its 10-month simple moving average, and rotates to the safe asset or cash otherwise." + ], + "counterfactuals": [ + "SPY: NOT traded because drift was 0.00%, at or below the 1.00% minimum-trade band; had drift exceeded that band the runner would have re-traded it back to target." + ], + "facts": [ + { + "rendered": "9", + "value": 9, + "source": "report.stages[*].ok" + }, + { + "rendered": "$103,437.44", + "value": 103437.44, + "source": "report.equity" + }, + { + "rendered": "10", + "value": 10, + "source": "rule_constant.trend.n_months" + }, + { + "rendered": "100.00%", + "value": 1.0, + "source": "report.target_weights.SPY" + }, + { + "rendered": "0.00%", + "value": 6.408124562096162e-07, + "source": "derived.abs(report.plan.skipped[0].diff)" + }, + { + "rendered": "1.00%", + "value": 0.01, + "source": "report.plan.min_trade_frac" + }, + { + "rendered": "0.0000", + "value": 0.0, + "source": "report.plan.est_turnover" + } + ], + "disclaimer": "Generated from this run's structured fields and the strategy's pre-registered rule parameters only. It contains no market commentary, no news, and no inferred reasoning: every number above is traceable to a named source field.", + "available": true, + "note": null +} \ No newline at end of file diff --git a/frontend/public/snapshot/api-runs-run_trend_20260814T140007Z-narrate.json b/frontend/public/snapshot/api-runs-run_trend_20260814T140007Z-narrate.json new file mode 100644 index 0000000..b7b45bd --- /dev/null +++ b/frontend/public/snapshot/api-runs-run_trend_20260814T140007Z-narrate.json @@ -0,0 +1,51 @@ +{ + "run_id": "run_trend_20260814T140007Z", + "strategy": "trend", + "narration": "Run run_trend_20260814T140007Z: account trend executed at 2026-08-14T14:00:07.702473Z as a LIVE SUBMIT against the paper broker. The gated pipeline completed 9 stages without aborting. Account equity was read as $103,530.68. Rule: trend holds the risk asset while its price is above its 10-month simple moving average, and rotates to the safe asset or cash otherwise. That rule produced a target of SPY at 100.00%. No orders were planned: every holding was already inside the minimum-trade band, so the run left the account untouched. Estimated turnover for the plan was 0.0000 of equity. Branches the rule did not take: SPY: NOT traded because drift was 0.00%, at or below the 1.00% minimum-trade band; had drift exceeded that band the runner would have re-traded it back to target.", + "rule_sentences": [ + "Rule: trend holds the risk asset while its price is above its 10-month simple moving average, and rotates to the safe asset or cash otherwise." + ], + "counterfactuals": [ + "SPY: NOT traded because drift was 0.00%, at or below the 1.00% minimum-trade band; had drift exceeded that band the runner would have re-traded it back to target." + ], + "facts": [ + { + "rendered": "9", + "value": 9, + "source": "report.stages[*].ok" + }, + { + "rendered": "$103,530.68", + "value": 103530.68, + "source": "report.equity" + }, + { + "rendered": "10", + "value": 10, + "source": "rule_constant.trend.n_months" + }, + { + "rendered": "100.00%", + "value": 1.0, + "source": "report.target_weights.SPY" + }, + { + "rendered": "0.00%", + "value": 7.099344778715988e-09, + "source": "derived.abs(report.plan.skipped[0].diff)" + }, + { + "rendered": "1.00%", + "value": 0.01, + "source": "report.plan.min_trade_frac" + }, + { + "rendered": "0.0000", + "value": 0.0, + "source": "report.plan.est_turnover" + } + ], + "disclaimer": "Generated from this run's structured fields and the strategy's pre-registered rule parameters only. It contains no market commentary, no news, and no inferred reasoning: every number above is traceable to a named source field.", + "available": true, + "note": null +} \ No newline at end of file diff --git a/frontend/public/snapshot/api-runs-run_voltarget_20260811T140005Z-narrate.json b/frontend/public/snapshot/api-runs-run_voltarget_20260811T140005Z-narrate.json new file mode 100644 index 0000000..63a7616 --- /dev/null +++ b/frontend/public/snapshot/api-runs-run_voltarget_20260811T140005Z-narrate.json @@ -0,0 +1,86 @@ +{ + "run_id": "run_voltarget_20260811T140005Z", + "strategy": "voltarget", + "narration": "Run run_voltarget_20260811T140005Z: account voltarget executed at 2026-08-11T14:00:05.419719Z as a LIVE SUBMIT against the paper broker. The gated pipeline completed 10 stages without aborting. Account equity was read as $102,714.05. Rule: voltarget sizes exposure as its target volatility (10%) divided by trailing 20-day realized volatility, annualized on a 252-day year, capped at 100% of equity; the remainder is held in cash. That rule produced a target of SPY at 71.41%. Planned orders: BUY SPY for $1,487.39 notional moving weight from 69.96% to 71.41%. Estimated turnover for the plan was 0.0145 of equity. Submitted to the broker: buy SPY for $1,487.39 \u2014 broker status 'pending_new'. Branches the rule did not take: SPY: traded because drift was 1.45%, above the 1.00% minimum-trade band; had drift been at or below that band the runner would have left the position to drift untouched, placing no order.", + "rule_sentences": [ + "Rule: voltarget sizes exposure as its target volatility (10%) divided by trailing 20-day realized volatility, annualized on a 252-day year, capped at 100% of equity; the remainder is held in cash." + ], + "counterfactuals": [ + "SPY: traded because drift was 1.45%, above the 1.00% minimum-trade band; had drift been at or below that band the runner would have left the position to drift untouched, placing no order." + ], + "facts": [ + { + "rendered": "10", + "value": 10, + "source": "report.stages[*].ok" + }, + { + "rendered": "$102,714.05", + "value": 102714.05, + "source": "report.equity" + }, + { + "rendered": "10%", + "value": 0.1, + "source": "rule_constant.voltarget.target_vol" + }, + { + "rendered": "20", + "value": 20, + "source": "rule_constant.voltarget.lookback_days" + }, + { + "rendered": "252", + "value": 252, + "source": "rule_constant.voltarget.periods_per_year" + }, + { + "rendered": "100%", + "value": 1.0, + "source": "rule_constant.voltarget.max_weight" + }, + { + "rendered": "71.41%", + "value": 0.7140967253638115, + "source": "report.target_weights.SPY" + }, + { + "rendered": "$1,487.39", + "value": 1487.3944318548065, + "source": "report.plan.intents[0].notional" + }, + { + "rendered": "69.96%", + "value": 0.6996158005842433, + "source": "report.plan.intents[0].current_w" + }, + { + "rendered": "71.41%", + "value": 0.7140967253638115, + "source": "report.plan.intents[0].target_w" + }, + { + "rendered": "1.45%", + "value": 0.014480924779568194, + "source": "derived.abs(report.plan.intents[0].target_w - report.plan.intents[0].current_w)" + }, + { + "rendered": "1.00%", + "value": 0.01, + "source": "report.plan.min_trade_frac" + }, + { + "rendered": "0.0145", + "value": 0.014480924779568194, + "source": "report.plan.est_turnover" + }, + { + "rendered": "$1,487.39", + "value": 1487.39, + "source": "report.submitted_orders[0].notional" + } + ], + "disclaimer": "Generated from this run's structured fields and the strategy's pre-registered rule parameters only. It contains no market commentary, no news, and no inferred reasoning: every number above is traceable to a named source field.", + "available": true, + "note": null +} \ No newline at end of file diff --git a/frontend/public/snapshot/api-runs-run_voltarget_20260812T140025Z-narrate.json b/frontend/public/snapshot/api-runs-run_voltarget_20260812T140025Z-narrate.json new file mode 100644 index 0000000..3434859 --- /dev/null +++ b/frontend/public/snapshot/api-runs-run_voltarget_20260812T140025Z-narrate.json @@ -0,0 +1,66 @@ +{ + "run_id": "run_voltarget_20260812T140025Z", + "strategy": "voltarget", + "narration": "Run run_voltarget_20260812T140025Z: account voltarget executed at 2026-08-12T14:00:25.624405Z as a LIVE SUBMIT against the paper broker. The gated pipeline completed 9 stages without aborting. Account equity was read as $102,594.59. Rule: voltarget sizes exposure as its target volatility (10%) divided by trailing 20-day realized volatility, annualized on a 252-day year, capped at 100% of equity; the remainder is held in cash. That rule produced a target of SPY at 71.00%. No orders were planned: every holding was already inside the minimum-trade band, so the run left the account untouched. Estimated turnover for the plan was 0.0000 of equity. Branches the rule did not take: SPY: NOT traded because drift was 0.38%, at or below the 1.00% minimum-trade band; had drift exceeded that band the runner would have re-traded it back to target.", + "rule_sentences": [ + "Rule: voltarget sizes exposure as its target volatility (10%) divided by trailing 20-day realized volatility, annualized on a 252-day year, capped at 100% of equity; the remainder is held in cash." + ], + "counterfactuals": [ + "SPY: NOT traded because drift was 0.38%, at or below the 1.00% minimum-trade band; had drift exceeded that band the runner would have re-traded it back to target." + ], + "facts": [ + { + "rendered": "9", + "value": 9, + "source": "report.stages[*].ok" + }, + { + "rendered": "$102,594.59", + "value": 102594.59, + "source": "report.equity" + }, + { + "rendered": "10%", + "value": 0.1, + "source": "rule_constant.voltarget.target_vol" + }, + { + "rendered": "20", + "value": 20, + "source": "rule_constant.voltarget.lookback_days" + }, + { + "rendered": "252", + "value": 252, + "source": "rule_constant.voltarget.periods_per_year" + }, + { + "rendered": "100%", + "value": 1.0, + "source": "rule_constant.voltarget.max_weight" + }, + { + "rendered": "71.00%", + "value": 0.7100286644718531, + "source": "report.target_weights.SPY" + }, + { + "rendered": "0.38%", + "value": 0.0037579871634817463, + "source": "derived.abs(report.plan.skipped[0].diff)" + }, + { + "rendered": "1.00%", + "value": 0.01, + "source": "report.plan.min_trade_frac" + }, + { + "rendered": "0.0000", + "value": 0.0, + "source": "report.plan.est_turnover" + } + ], + "disclaimer": "Generated from this run's structured fields and the strategy's pre-registered rule parameters only. It contains no market commentary, no news, and no inferred reasoning: every number above is traceable to a named source field.", + "available": true, + "note": null +} \ No newline at end of file diff --git a/frontend/public/snapshot/api-runs-run_voltarget_20260813T140004Z-narrate.json b/frontend/public/snapshot/api-runs-run_voltarget_20260813T140004Z-narrate.json new file mode 100644 index 0000000..5b8dfec --- /dev/null +++ b/frontend/public/snapshot/api-runs-run_voltarget_20260813T140004Z-narrate.json @@ -0,0 +1,66 @@ +{ + "run_id": "run_voltarget_20260813T140004Z", + "strategy": "voltarget", + "narration": "Run run_voltarget_20260813T140004Z: account voltarget executed at 2026-08-13T14:00:04.756492Z as a LIVE SUBMIT against the paper broker. The gated pipeline completed 9 stages without aborting. Account equity was read as $103,089.56. Rule: voltarget sizes exposure as its target volatility (10%) divided by trailing 20-day realized volatility, annualized on a 252-day year, capped at 100% of equity; the remainder is held in cash. That rule produced a target of SPY at 71.14%. No orders were planned: every holding was already inside the minimum-trade band, so the run left the account untouched. Estimated turnover for the plan was 0.0000 of equity. Branches the rule did not take: SPY: NOT traded because drift was 0.37%, at or below the 1.00% minimum-trade band; had drift exceeded that band the runner would have re-traded it back to target.", + "rule_sentences": [ + "Rule: voltarget sizes exposure as its target volatility (10%) divided by trailing 20-day realized volatility, annualized on a 252-day year, capped at 100% of equity; the remainder is held in cash." + ], + "counterfactuals": [ + "SPY: NOT traded because drift was 0.37%, at or below the 1.00% minimum-trade band; had drift exceeded that band the runner would have re-traded it back to target." + ], + "facts": [ + { + "rendered": "9", + "value": 9, + "source": "report.stages[*].ok" + }, + { + "rendered": "$103,089.56", + "value": 103089.56, + "source": "report.equity" + }, + { + "rendered": "10%", + "value": 0.1, + "source": "rule_constant.voltarget.target_vol" + }, + { + "rendered": "20", + "value": 20, + "source": "rule_constant.voltarget.lookback_days" + }, + { + "rendered": "252", + "value": 252, + "source": "rule_constant.voltarget.periods_per_year" + }, + { + "rendered": "100%", + "value": 1.0, + "source": "rule_constant.voltarget.max_weight" + }, + { + "rendered": "71.14%", + "value": 0.7114196161850412, + "source": "report.target_weights.SPY" + }, + { + "rendered": "0.37%", + "value": 0.0037320015539421547, + "source": "derived.abs(report.plan.skipped[0].diff)" + }, + { + "rendered": "1.00%", + "value": 0.01, + "source": "report.plan.min_trade_frac" + }, + { + "rendered": "0.0000", + "value": 0.0, + "source": "report.plan.est_turnover" + } + ], + "disclaimer": "Generated from this run's structured fields and the strategy's pre-registered rule parameters only. It contains no market commentary, no news, and no inferred reasoning: every number above is traceable to a named source field.", + "available": true, + "note": null +} \ No newline at end of file diff --git a/frontend/public/snapshot/api-runs-run_voltarget_20260814T140004Z-narrate.json b/frontend/public/snapshot/api-runs-run_voltarget_20260814T140004Z-narrate.json new file mode 100644 index 0000000..8efd0ce --- /dev/null +++ b/frontend/public/snapshot/api-runs-run_voltarget_20260814T140004Z-narrate.json @@ -0,0 +1,66 @@ +{ + "run_id": "run_voltarget_20260814T140004Z", + "strategy": "voltarget", + "narration": "Run run_voltarget_20260814T140004Z: account voltarget executed at 2026-08-14T14:00:04.460187Z as a LIVE SUBMIT against the paper broker. The gated pipeline completed 9 stages without aborting. Account equity was read as $103,171.09. Rule: voltarget sizes exposure as its target volatility (10%) divided by trailing 20-day realized volatility, annualized on a 252-day year, capped at 100% of equity; the remainder is held in cash. That rule produced a target of SPY at 71.58%. No orders were planned: every holding was already inside the minimum-trade band, so the run left the account untouched. Estimated turnover for the plan was 0.0000 of equity. Branches the rule did not take: SPY: NOT traded because drift was 0.04%, at or below the 1.00% minimum-trade band; had drift exceeded that band the runner would have re-traded it back to target.", + "rule_sentences": [ + "Rule: voltarget sizes exposure as its target volatility (10%) divided by trailing 20-day realized volatility, annualized on a 252-day year, capped at 100% of equity; the remainder is held in cash." + ], + "counterfactuals": [ + "SPY: NOT traded because drift was 0.04%, at or below the 1.00% minimum-trade band; had drift exceeded that band the runner would have re-traded it back to target." + ], + "facts": [ + { + "rendered": "9", + "value": 9, + "source": "report.stages[*].ok" + }, + { + "rendered": "$103,171.09", + "value": 103171.09, + "source": "report.equity" + }, + { + "rendered": "10%", + "value": 0.1, + "source": "rule_constant.voltarget.target_vol" + }, + { + "rendered": "20", + "value": 20, + "source": "rule_constant.voltarget.lookback_days" + }, + { + "rendered": "252", + "value": 252, + "source": "rule_constant.voltarget.periods_per_year" + }, + { + "rendered": "100%", + "value": 1.0, + "source": "rule_constant.voltarget.max_weight" + }, + { + "rendered": "71.58%", + "value": 0.7157564384670008, + "source": "report.target_weights.SPY" + }, + { + "rendered": "0.04%", + "value": 0.00037967442389519324, + "source": "derived.abs(report.plan.skipped[0].diff)" + }, + { + "rendered": "1.00%", + "value": 0.01, + "source": "report.plan.min_trade_frac" + }, + { + "rendered": "0.0000", + "value": 0.0, + "source": "report.plan.est_turnover" + } + ], + "disclaimer": "Generated from this run's structured fields and the strategy's pre-registered rule parameters only. It contains no market commentary, no news, and no inferred reasoning: every number above is traceable to a named source field.", + "available": true, + "note": null +} \ No newline at end of file diff --git a/frontend/public/snapshot/api-runs.json b/frontend/public/snapshot/api-runs.json index 43675e9..975d87c 100644 --- a/frontend/public/snapshot/api-runs.json +++ b/frontend/public/snapshot/api-runs.json @@ -1,7 +1,1259 @@ { - "count": 115, + "count": 133, "label": null, "runs": [ + { + "run_id": "run_crypto_voltarget_20260815T003008Z", + "strategy": "crypto_voltarget", + "timestamp": "2026-08-15T00:30:08.016859Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 98246.87, + "target_weights": { + "BTC-USD": 0.8897688975075773 + }, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested BTC-USD" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated BTC-USD" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=98246.87 cash=10511.63" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-14 -> {'BTC-USD': 0.8897688975075773}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=-0.04117337572279045)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_crypto_trend_20260815T003005Z", + "strategy": "crypto_trend", + "timestamp": "2026-08-15T00:30:05.871787Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 100000.0, + "target_weights": {}, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested BTC-USD" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated BTC-USD" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=100000.00 cash=100000.00" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-14 -> {}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=0.0)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_trend_20260814T140007Z", + "strategy": "trend", + "timestamp": "2026-08-14T14:00:07.702473Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 103530.68, + "target_weights": { + "SPY": 1.0 + }, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested SPY, IEF" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated SPY, IEF" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=103530.68 cash=0.00" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-13 -> {'SPY': 1.0}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=0.0)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_voltarget_20260814T140004Z", + "strategy": "voltarget", + "timestamp": "2026-08-14T14:00:04.460187Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 103171.09, + "target_weights": { + "SPY": 0.7157564384670008 + }, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested SPY" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated SPY" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=103171.09 cash=29364.89" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-13 -> {'SPY': 0.7157564384670008}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=0.0)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_crypto_voltarget_20260814T063813Z", + "strategy": "crypto_voltarget", + "timestamp": "2026-08-14T06:38:13.328622Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 98535.82, + "target_weights": { + "BTC-USD": 0.8939608815069376 + }, + "no_trades": false, + "est_turnover": 0.032861717365410215, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested BTC-USD" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated BTC-USD" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=98535.82 cash=13686.71" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-13 -> {'BTC-USD': 0.8939608815069376}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=-0.038353408500578556)" + }, + { + "stage": "plan", + "ok": true, + "detail": "1 intent(s), buy_scale=1.0000, turnover=0.0329" + }, + { + "stage": "submit", + "ok": true, + "detail": "submitted 1 order(s), 0 duplicate(s)" + } + ], + "intents": [ + { + "symbol": "BTC-USD", + "side": "buy", + "notional": 3238.0562672089354, + "current_w": 0.8610991641415274, + "target_w": 0.8939608815069376 + } + ], + "submitted_orders": [ + { + "symbol": "BTC-USD", + "side": "buy", + "notional": 3238.06, + "status": "pending_new", + "client_order_id": "ql-crypto_voltarget-20260814-BTC-USD-buy", + "submitted_at": "2026-08-14T06:38:15.042685Z", + "was_duplicate": false + } + ] + }, + { + "run_id": "run_crypto_trend_20260814T063810Z", + "strategy": "crypto_trend", + "timestamp": "2026-08-14T06:38:10.490942Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 100000.0, + "target_weights": {}, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested BTC-USD" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated BTC-USD" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=100000.00 cash=100000.00" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-13 -> {}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=0.0)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_trend_20260813T140007Z", + "strategy": "trend", + "timestamp": "2026-08-13T14:00:07.974910Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 103437.44, + "target_weights": { + "SPY": 1.0 + }, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested SPY, IEF" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated SPY, IEF" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=103437.44 cash=0.00" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-12 -> {'SPY': 1.0}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=0.0)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_voltarget_20260813T140004Z", + "strategy": "voltarget", + "timestamp": "2026-08-13T14:00:04.756492Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 103089.56, + "target_weights": { + "SPY": 0.7114196161850412 + }, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested SPY" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated SPY" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=103089.56 cash=29364.89" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-12 -> {'SPY': 0.7114196161850412}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=0.0)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_crypto_voltarget_20260813T012744Z", + "strategy": "crypto_voltarget", + "timestamp": "2026-08-13T01:27:44.649765Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 98758.87, + "target_weights": { + "BTC-USD": 0.8620658490902996 + }, + "no_trades": false, + "est_turnover": 0.03276930170169534, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested BTC-USD" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated BTC-USD" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=98758.87 cash=16858.48" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-12 -> {'BTC-USD': 0.8620658490902996}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=-0.03617658313662531)" + }, + { + "stage": "plan", + "ok": true, + "detail": "1 intent(s), buy_scale=1.0000, turnover=0.0328" + }, + { + "stage": "submit", + "ok": true, + "detail": "submitted 1 order(s), 0 duplicate(s)" + } + ], + "intents": [ + { + "symbol": "BTC-USD", + "side": "buy", + "notional": 3236.259206748509, + "current_w": 0.8292965473886043, + "target_w": 0.8620658490902996 + } + ], + "submitted_orders": [ + { + "symbol": "BTC-USD", + "side": "buy", + "notional": 3236.26, + "status": "pending_new", + "client_order_id": "ql-crypto_voltarget-20260813-BTC-USD-buy", + "submitted_at": "2026-08-13T01:27:46.057365Z", + "was_duplicate": false + } + ] + }, + { + "run_id": "run_crypto_trend_20260813T012741Z", + "strategy": "crypto_trend", + "timestamp": "2026-08-13T01:27:41.385485Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 100000.0, + "target_weights": {}, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested BTC-USD" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated BTC-USD" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=100000.00 cash=100000.00" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-12 -> {}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=0.0)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_trend_20260812T140032Z", + "strategy": "trend", + "timestamp": "2026-08-12T14:00:32.018986Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 102752.34, + "target_weights": { + "SPY": 1.0 + }, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested SPY, IEF" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated SPY, IEF" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=102752.34 cash=0.00" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-11 -> {'SPY': 1.0}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=-0.0014996113954141022)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_voltarget_20260812T140025Z", + "strategy": "voltarget", + "timestamp": "2026-08-12T14:00:25.624405Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 102594.59, + "target_weights": { + "SPY": 0.7100286644718531 + }, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested SPY" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated SPY" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=102594.59 cash=29364.89" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-11 -> {'SPY': 0.7100286644718531}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=-0.001163034657868156)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_crypto_voltarget_20260812T123722Z", + "strategy": "crypto_voltarget", + "timestamp": "2026-08-12T12:37:22.272525Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 99810.63, + "target_weights": { + "BTC-USD": 0.832964732069648 + }, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested BTC-USD" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated BTC-USD" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=99810.63 cash=16858.48" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-11 -> {'BTC-USD': 0.832964732069648}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=-0.025912078116263726)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_crypto_trend_20260812T123719Z", + "strategy": "crypto_trend", + "timestamp": "2026-08-12T12:37:19.836866Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 100000.0, + "target_weights": {}, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested BTC-USD" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated BTC-USD" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=100000.00 cash=100000.00" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-11 -> {}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=0.0)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_trend_20260811T140012Z", + "strategy": "trend", + "timestamp": "2026-08-11T14:00:12.753204Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 102905.33, + "target_weights": { + "SPY": 1.0 + }, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested SPY, IEF" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated SPY, IEF" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=102905.33 cash=0.00" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-10 -> {'SPY': 1.0}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=-1.2924333566033397e-05)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, + { + "run_id": "run_voltarget_20260811T140005Z", + "strategy": "voltarget", + "timestamp": "2026-08-11T14:00:05.419719Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 102714.05, + "target_weights": { + "SPY": 0.7140967253638115 + }, + "no_trades": false, + "est_turnover": 0.014480924779568194, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested SPY" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated SPY" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=102714.05 cash=30852.28" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-10 -> {'SPY': 0.7140967253638115}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=0.0)" + }, + { + "stage": "plan", + "ok": true, + "detail": "1 intent(s), buy_scale=1.0000, turnover=0.0145" + }, + { + "stage": "submit", + "ok": true, + "detail": "submitted 1 order(s), 0 duplicate(s)" + } + ], + "intents": [ + { + "symbol": "SPY", + "side": "buy", + "notional": 1487.3944318548065, + "current_w": 0.6996158005842433, + "target_w": 0.7140967253638115 + } + ], + "submitted_orders": [ + { + "symbol": "SPY", + "side": "buy", + "notional": 1487.39, + "status": "pending_new", + "client_order_id": "ql-voltarget-20260811-SPY-buy", + "submitted_at": "2026-08-11T14:00:05.119384Z", + "was_duplicate": false + } + ] + }, + { + "run_id": "run_crypto_voltarget_20260811T111735Z", + "strategy": "crypto_voltarget", + "timestamp": "2026-08-11T11:17:35.257478Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 100040.42, + "target_weights": { + "BTC-USD": 0.8323567343776824 + }, + "no_trades": false, + "est_turnover": 0.044286670957316865, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested BTC-USD" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated BTC-USD" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=100040.42 cash=21201.56" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-10 -> {'BTC-USD': 0.8323567343776824}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=-0.023669474662406653)" + }, + { + "stage": "plan", + "ok": true, + "detail": "1 intent(s), buy_scale=1.0000, turnover=0.0443" + }, + { + "stage": "submit", + "ok": true, + "detail": "submitted 1 order(s), 0 duplicate(s)" + } + ], + "intents": [ + { + "symbol": "BTC-USD", + "side": "buy", + "notional": 4430.457162971781, + "current_w": 0.7880700634203656, + "target_w": 0.8323567343776824 + } + ], + "submitted_orders": [ + { + "symbol": "BTC-USD", + "side": "buy", + "notional": 4430.46, + "status": "pending_new", + "client_order_id": "ql-crypto_voltarget-20260811-BTC-USD-buy", + "submitted_at": "2026-08-11T11:17:34.415849Z", + "was_duplicate": false + } + ] + }, + { + "run_id": "run_crypto_trend_20260811T111731Z", + "strategy": "crypto_trend", + "timestamp": "2026-08-11T11:17:31.349349Z", + "dry_run": false, + "aborted": false, + "abort_stage": null, + "abort_reason": null, + "equity": 100000.0, + "target_weights": {}, + "no_trades": true, + "est_turnover": 0.0, + "min_trade_frac": 0.01, + "stages": [ + { + "stage": "risk_state", + "ok": true, + "detail": "not halted" + }, + { + "stage": "ingest", + "ok": true, + "detail": "ingested BTC-USD" + }, + { + "stage": "validate", + "ok": true, + "detail": "validated BTC-USD" + }, + { + "stage": "health", + "ok": true, + "detail": "data fresh" + }, + { + "stage": "account", + "ok": true, + "detail": "equity=100000.00 cash=100000.00" + }, + { + "stage": "target_weights", + "ok": true, + "detail": "signal@2026-08-10 -> {}" + }, + { + "stage": "check_weights", + "ok": true, + "detail": "no adjustment" + }, + { + "stage": "evaluate_portfolio", + "ok": true, + "detail": "ALLOW (dd=0.0)" + }, + { + "stage": "plan", + "ok": true, + "detail": "in-band, no trades" + } + ], + "intents": [], + "submitted_orders": [] + }, { "run_id": "run_trend_20260810T140010Z", "strategy": "trend", diff --git a/frontend/public/snapshot/api-timeline.json b/frontend/public/snapshot/api-timeline.json index 13a14e9..bff4a05 100644 --- a/frontend/public/snapshot/api-timeline.json +++ b/frontend/public/snapshot/api-timeline.json @@ -1,6 +1,166 @@ { - "count": 352, + "count": 378, "events": [ + { + "at": "2026-08-15T16:24:57.881659Z", + "kind": "alert", + "label": null, + "title": "glass box refresh dry run \u2014 gates passed, not deployed", + "detail": "========================================================================\nGLASS BOX REFRESH \u2014 CHAIN REPORT\n========================================================================\nstarted : 2026-08-15T16:24:46.341653+00:00\nmode : DRY RUN (stops before deploy)\nsite : monzonautomation-glassbox (be63f48c-4949-4603-b8dd-a6ccfdd996e7)\n\nCHAIN (each step runs only if every earlier step passed)\n------------------------------------------------------------------------\n snapshot PASS 153 endpoint(s) captured, 154 file(s) written, 0 redaction(s)\n build PASS public bundle built into frontend/dist\n verify-dist PASS 178 published file(s) scanned, 0 forbidden, 0 redactable finding(s)\n deploy SKIPPED withheld (--dry-run); all gates passed\n\nPROVENANCE\n------------------------------------------------------------------------\n repo state : WARNING\n working tree is DIRTY: 41 path(s) differ from HEAD [docs/decisions.md, frontend/public/snapshot/api-decisions.json, frontend/public/snapshot/api-divergence--label-crypto_trend.json, frontend/public/snapshot/api-divergence--label-crypto_voltarget.json, frontend/public/snapshot/api-divergence--label-trend.json (+36 more)] \u2014 artifacts will record a commit that does not contain them\n\nDEPLOY DECISION\n------------------------------------------------------------------------\n forbidden matches : 0 ok\n redactions : 0 ok\n redactable found : 0 ok\n NOT DEPLOYED \u2014 dry run stopped before the deploy step\n\nSNAPSHOT SANITIZATION REPORT\n------------------------------------------------------------------------\n========================================================================\nGLASS BOX SNAPSHOT \u2014 SANITIZATION REPORT\n========================================================================\nfiles scanned : 154\nbytes scanned : 1,255,347\nverdict : PASS\n\nFORBIDDEN PATTERNS (zero matches required to pass)\n------------------------------------------------------------------------\n alpaca_account_id 0 ok\n email_address 0 ok\n apca_api_header 0 ok\n authorization_header 0 ok\n env_secret_prefix:ALPACA_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_TREND_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_TREND_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_VOLTARGET_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_VOLTARGET_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_TREND_API_KEY 0 ok\n env_secret_prefix:ALPACA_TREND_SECRET_KEY 0 ok\n env_secret_prefix:SMTP_PASS 0 ok\n env_secret_prefix:TIINGO_API_KEY 0 ok\n\nENV-SECRET CHECK\n------------------------------------------------------------------------\n .env read locally; 10 value(s) reduced to 8-char prefixes and searched.\n keys checked: ALPACA_API_KEY, ALPACA_CRYPTO_TREND_API_KEY, ALPACA_CRYPTO_TREND_SECRET_KEY, ALPACA_CRYPTO_VOLTARGET_API_KEY, ALPACA_CRYPTO_VOLTARGET_SECRET_KEY, ALPACA_SECRET_KEY, ALPACA_TREND_API_KEY, ALPACA_TREND_SECRET_KEY, SMTP_PASS, TIINGO_API_KEY\n keys EXCLUDED as non-secret: ALERT_EMAIL_TO, ALPACA_BASE_URL, SMTP_HOST, SMTP_PORT, SMTP_USER\n (public URLs and documented endpoints; see is_secret_bearing)\n (prefixes themselves are never printed or stored)\n\nREDACTIONS PERFORMED (0)\n------------------------------------------------------------------------\n (none)\n\n========================================================================\nPASS \u2014 no forbidden pattern matched. Snapshot may be written.\n========================================================================\n\nPUBLISHED-BYTES GATE (verify-dist)\n------------------------------------------------------------------------\n========================================================================\nGLASS BOX DIST VERIFICATION \u2014 published bytes\n========================================================================\ndirectory : .\\frontend\\dist\ntext files : 178 scanned\nbinary files : 12 skipped (not text-scannable)\n assets/fraunces-latin-ext-standard-italic-CGbN9UgK.woff2, assets/fraunces-latin-ext-standard-normal-CJcjJNj7.woff2, assets/fraunces-latin-standard-italic-lSdLDfvT.woff2, assets/fraunces-latin-standard-normal-DihXLNYH.woff2, assets/fraunces-vietnamese-standard-italic-DxWqP7Ku.woff2, assets/fraunces-vietnamese-standard-normal-Czevyj-6.woff2, assets/hanken-grotesk-cyrillic-ext-wght-normal-D_UHSL_T.woff2, assets/hanken-grotesk-latin-ext-wght-normal-Dg-wlmqe.woff2, assets/hanken-grotesk-latin-wght-normal-CaVRRdDk.woff2, assets/hanken-grotesk-vietnamese-wght-normal-CHiFlh_0.woff2, favicon.ico, og-image.png\n\nCONTENT COMPLETENESS (the site must contain what it claims to)\n------------------------------------------------------------------------\n manifest_present ok\n snapshot/manifest.json parsed; quantlab 1.0.0 @ 9910554\n endpoint_count ok\n 153 endpoints declared, listed, and present on disk\n manifest_freshness ok\n captured 2026-08-15T16:24:47.605455+00:00 \u2014 -0.0h old (limit 14d)\n overview_has_data ok\n 4 of 4 account(s) carry a non-null equity figure\n entry_bundle ok\n 1 entry script(s) referenced and present: index-17GVHUPC.js\n\n CONTENT PASS \u2014 5 assertion(s) satisfied.\n\n========================================================================\nGLASS BOX SNAPSHOT \u2014 SANITIZATION REPORT\n========================================================================\nfiles scanned : 178\nbytes scanned : 2,007,788\nverdict : PASS\n\nFORBIDDEN PATTERNS (zero matches required to pass)\n------------------------------------------------------------------------\n alpaca_account_id 0 ok\n email_address 0 ok\n apca_api_header 0 ok\n authorization_header 0 ok\n env_secret_prefix:ALPACA_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_TREND_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_TREND_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_VOLTARGET_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_VOLTARGET_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_TREND_API_KEY 0 ok\n env_secret_prefix:ALPACA_TREND_SECRET_KEY 0 ok\n env_secret_prefix:SMTP_PASS 0 ok\n env_secret_prefix:TIINGO_API_KEY 0 ok\n\nENV-SECRET CHECK\n------------------------------------------------------------------------\n .env read locally; 10 value(s) reduced to 8-char prefixes and searched.\n keys checked: ALPACA_API_KEY, ALPACA_CRYPTO_TREND_API_KEY, ALPACA_CRYPTO_TREND_SECRET_KEY, ALPACA_CRYPTO_VOLTARGET_API_KEY, ALPACA_CRYPTO_VOLTARGET_SECRET_KEY, ALPACA_SECRET_KEY, ALPACA_TREND_API_KEY, ALPACA_TREND_SECRET_KEY, SMTP_PASS, TIINGO_API_KEY\n keys EXCLUDED as non-secret: ALERT_EMAIL_TO, ALPACA_BASE_URL, SMTP_HOST, SMTP_PORT, SMTP_USER\n (public URLs and documented endpoints; see is_secret_bearing)\n (prefixes themselves are never printed or stored)\n\nREDACTIONS PERFORMED (0)\n------------------------------------------------------------------------\n (none)\n\n========================================================================\nPASS \u2014 no forbidden pattern matched. Snapshot may be written.\n========================================================================\n\nREDACTABLE CONTENT IN PUBLISHED BYTES (0)\n------------------------------------------------------------------------\n (none \u2014 no local path leaked into the build)\n\n========================================================================\nDIST PASS \u2014 safe to deploy, subject to human review of this report.\n========================================================================\n\n========================================================================\nDRY RUN \u2014 gates passed, deploy withheld\n========================================================================", + "level": "INFO" + }, + { + "at": "2026-08-15T00:00:00Z", + "kind": "decision", + "label": null, + "title": "An unchecked secret gate is an abort, not a note (automated chain only)", + "detail": "docs/decisions.md", + "level": null + }, + { + "at": "2026-08-15T00:00:00Z", + "kind": "decision", + "label": null, + "title": "The AI improvement pipeline, and the firewall that makes it safe", + "detail": "docs/decisions.md", + "level": null + }, + { + "at": "2026-08-15T00:00:00Z", + "kind": "decision", + "label": null, + "title": "DMARC enforcement: `p=none` -> `p=quarantine`", + "detail": "docs/decisions.md", + "level": null + }, + { + "at": "2026-08-14T21:49:12.349698Z", + "kind": "alert", + "label": null, + "title": "glass box refreshed and deployed", + "detail": "========================================================================\nGLASS BOX REFRESH \u2014 CHAIN REPORT\n========================================================================\nstarted : 2026-08-14T21:47:48.347735+00:00\nmode : LIVE\nsite : monzonautomation-glassbox (be63f48c-4949-4603-b8dd-a6ccfdd996e7)\n\nCHAIN (each step runs only if every earlier step passed)\n------------------------------------------------------------------------\n snapshot PASS 151 endpoint(s) captured, 152 file(s) written, 0 redaction(s)\n build PASS public bundle built into frontend/dist\n verify-dist PASS 176 published file(s) scanned, 0 forbidden, 0 redactable finding(s)\n deploy PASS published to \n\nPROVENANCE\n------------------------------------------------------------------------\n repo state : WARNING\n working tree is DIRTY: 36 path(s) differ from HEAD [frontend/public/snapshot/api-decisions.json, frontend/public/snapshot/api-divergence--label-crypto_trend.json, frontend/public/snapshot/api-divergence--label-crypto_voltarget.json, frontend/public/snapshot/api-divergence--label-trend.json, frontend/public/snapshot/api-divergence--label-voltarget.json (+31 more)] \u2014 artifacts will record a commit that does not contain them\n\nDEPLOY DECISION\n------------------------------------------------------------------------\n forbidden matches : 0 ok\n redactions : 0 ok\n redactable found : 0 ok\n DEPLOYED -> \n\nSNAPSHOT SANITIZATION REPORT\n------------------------------------------------------------------------\n========================================================================\nGLASS BOX SNAPSHOT \u2014 SANITIZATION REPORT\n========================================================================\nfiles scanned : 152\nbytes scanned : 1,230,245\nverdict : PASS\n\nFORBIDDEN PATTERNS (zero matches required to pass)\n------------------------------------------------------------------------\n alpaca_account_id 0 ok\n email_address 0 ok\n apca_api_header 0 ok\n authorization_header 0 ok\n env_secret_prefix:ALPACA_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_TREND_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_TREND_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_VOLTARGET_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_VOLTARGET_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_TREND_API_KEY 0 ok\n env_secret_prefix:ALPACA_TREND_SECRET_KEY 0 ok\n env_secret_prefix:SMTP_PASS 0 ok\n env_secret_prefix:TIINGO_API_KEY 0 ok\n\nENV-SECRET CHECK\n------------------------------------------------------------------------\n .env read locally; 10 value(s) reduced to 8-char prefixes and searched.\n keys checked: ALPACA_API_KEY, ALPACA_CRYPTO_TREND_API_KEY, ALPACA_CRYPTO_TREND_SECRET_KEY, ALPACA_CRYPTO_VOLTARGET_API_KEY, ALPACA_CRYPTO_VOLTARGET_SECRET_KEY, ALPACA_SECRET_KEY, ALPACA_TREND_API_KEY, ALPACA_TREND_SECRET_KEY, SMTP_PASS, TIINGO_API_KEY\n keys EXCLUDED as non-secret: ALERT_EMAIL_TO, ALPACA_BASE_URL, SMTP_HOST, SMTP_PORT, SMTP_USER\n (public URLs and documented endpoints; see is_secret_bearing)\n (prefixes themselves are never printed or stored)\n\nREDACTIONS PERFORMED (0)\n------------------------------------------------------------------------\n (none)\n\n========================================================================\nPASS \u2014 no forbidden pattern matched. Snapshot may be written.\n========================================================================\n\nPUBLISHED-BYTES GATE (verify-dist)\n------------------------------------------------------------------------\n========================================================================\nGLASS BOX DIST VERIFICATION \u2014 published bytes\n========================================================================\ndirectory : .\\frontend\\dist\ntext files : 176 scanned\nbinary files : 12 skipped (not text-scannable)\n assets/fraunces-latin-ext-standard-italic-CGbN9UgK.woff2, assets/fraunces-latin-ext-standard-normal-CJcjJNj7.woff2, assets/fraunces-latin-standard-italic-lSdLDfvT.woff2, assets/fraunces-latin-standard-normal-DihXLNYH.woff2, assets/fraunces-vietnamese-standard-italic-DxWqP7Ku.woff2, assets/fraunces-vietnamese-standard-normal-Czevyj-6.woff2, assets/hanken-grotesk-cyrillic-ext-wght-normal-D_UHSL_T.woff2, assets/hanken-grotesk-latin-ext-wght-normal-Dg-wlmqe.woff2, assets/hanken-grotesk-latin-wght-normal-CaVRRdDk.woff2, assets/hanken-grotesk-vietnamese-wght-normal-CHiFlh_0.woff2, favicon.ico, og-image.png\n\nCONTENT COMPLETENESS (the site must contain what it claims to)\n------------------------------------------------------------------------\n manifest_present ok\n snapshot/manifest.json parsed; quantlab 1.0.0 @ 9910554\n endpoint_count ok\n 151 endpoints declared, listed, and present on disk\n manifest_freshness ok\n captured 2026-08-14T21:47:50.330066+00:00 \u2014 -0.0h old (limit 14d)\n overview_has_data ok\n 4 of 4 account(s) carry a non-null equity figure\n entry_bundle ok\n 1 entry script(s) referenced and present: index-17GVHUPC.js\n\n CONTENT PASS \u2014 5 assertion(s) satisfied.\n\n========================================================================\nGLASS BOX SNAPSHOT \u2014 SANITIZATION REPORT\n========================================================================\nfiles scanned : 176\nbytes scanned : 1,982,265\nverdict : PASS\n\nFORBIDDEN PATTERNS (zero matches required to pass)\n------------------------------------------------------------------------\n alpaca_account_id 0 ok\n email_address 0 ok\n apca_api_header 0 ok\n authorization_header 0 ok\n env_secret_prefix:ALPACA_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_TREND_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_TREND_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_VOLTARGET_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_VOLTARGET_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_TREND_API_KEY 0 ok\n env_secret_prefix:ALPACA_TREND_SECRET_KEY 0 ok\n env_secret_prefix:SMTP_PASS 0 ok\n env_secret_prefix:TIINGO_API_KEY 0 ok\n\nENV-SECRET CHECK\n------------------------------------------------------------------------\n .env read locally; 10 value(s) reduced to 8-char prefixes and searched.\n keys checked: ALPACA_API_KEY, ALPACA_CRYPTO_TREND_API_KEY, ALPACA_CRYPTO_TREND_SECRET_KEY, ALPACA_CRYPTO_VOLTARGET_API_KEY, ALPACA_CRYPTO_VOLTARGET_SECRET_KEY, ALPACA_SECRET_KEY, ALPACA_TREND_API_KEY, ALPACA_TREND_SECRET_KEY, SMTP_PASS, TIINGO_API_KEY\n keys EXCLUDED as non-secret: ALERT_EMAIL_TO, ALPACA_BASE_URL, SMTP_HOST, SMTP_PORT, SMTP_USER\n (public URLs and documented endpoints; see is_secret_bearing)\n (prefixes themselves are never printed or stored)\n\nREDACTIONS PERFORMED (0)\n------------------------------------------------------------------------\n (none)\n\n========================================================================\nPASS \u2014 no forbidden pattern matched. Snapshot may be written.\n========================================================================\n\nREDACTABLE CONTENT IN PUBLISHED BYTES (0)\n------------------------------------------------------------------------\n (none \u2014 no local path leaked into the build)\n\n========================================================================\nDIST PASS \u2014 safe to deploy, subject to human review of this report.\n========================================================================\n\n========================================================================\nDEPLOYED\n========================================================================", + "level": "INFO" + }, + { + "at": "2026-08-14T21:46:18.753213Z", + "kind": "alert", + "label": null, + "title": "glass box refresh dry run \u2014 gates passed, not deployed", + "detail": "========================================================================\nGLASS BOX REFRESH \u2014 CHAIN REPORT\n========================================================================\nstarted : 2026-08-14T21:46:04.662252+00:00\nmode : DRY RUN (stops before deploy)\nsite : monzonautomation-glassbox (be63f48c-4949-4603-b8dd-a6ccfdd996e7)\n\nCHAIN (each step runs only if every earlier step passed)\n------------------------------------------------------------------------\n snapshot PASS 151 endpoint(s) captured, 152 file(s) written, 0 redaction(s)\n build PASS public bundle built into frontend/dist\n verify-dist PASS 176 published file(s) scanned, 0 forbidden, 0 redactable finding(s)\n deploy SKIPPED withheld (--dry-run); all gates passed\n\nPROVENANCE\n------------------------------------------------------------------------\n repo state : WARNING\n working tree is DIRTY: 36 path(s) differ from HEAD [frontend/public/snapshot/api-decisions.json, frontend/public/snapshot/api-divergence--label-crypto_trend.json, frontend/public/snapshot/api-divergence--label-crypto_voltarget.json, frontend/public/snapshot/api-divergence--label-trend.json, frontend/public/snapshot/api-divergence--label-voltarget.json (+31 more)] \u2014 artifacts will record a commit that does not contain them\n\nDEPLOY DECISION\n------------------------------------------------------------------------\n forbidden matches : 0 ok\n redactions : 0 ok\n redactable found : 0 ok\n NOT DEPLOYED \u2014 dry run stopped before the deploy step\n\nSNAPSHOT SANITIZATION REPORT\n------------------------------------------------------------------------\n========================================================================\nGLASS BOX SNAPSHOT \u2014 SANITIZATION REPORT\n========================================================================\nfiles scanned : 152\nbytes scanned : 1,221,458\nverdict : PASS\n\nFORBIDDEN PATTERNS (zero matches required to pass)\n------------------------------------------------------------------------\n alpaca_account_id 0 ok\n email_address 0 ok\n apca_api_header 0 ok\n authorization_header 0 ok\n env_secret_prefix:ALPACA_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_TREND_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_TREND_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_VOLTARGET_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_VOLTARGET_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_TREND_API_KEY 0 ok\n env_secret_prefix:ALPACA_TREND_SECRET_KEY 0 ok\n env_secret_prefix:SMTP_PASS 0 ok\n env_secret_prefix:TIINGO_API_KEY 0 ok\n\nENV-SECRET CHECK\n------------------------------------------------------------------------\n .env read locally; 10 value(s) reduced to 8-char prefixes and searched.\n keys checked: ALPACA_API_KEY, ALPACA_CRYPTO_TREND_API_KEY, ALPACA_CRYPTO_TREND_SECRET_KEY, ALPACA_CRYPTO_VOLTARGET_API_KEY, ALPACA_CRYPTO_VOLTARGET_SECRET_KEY, ALPACA_SECRET_KEY, ALPACA_TREND_API_KEY, ALPACA_TREND_SECRET_KEY, SMTP_PASS, TIINGO_API_KEY\n keys EXCLUDED as non-secret: ALERT_EMAIL_TO, ALPACA_BASE_URL, SMTP_HOST, SMTP_PORT, SMTP_USER\n (public URLs and documented endpoints; see is_secret_bearing)\n (prefixes themselves are never printed or stored)\n\nREDACTIONS PERFORMED (0)\n------------------------------------------------------------------------\n (none)\n\n========================================================================\nPASS \u2014 no forbidden pattern matched. Snapshot may be written.\n========================================================================\n\nPUBLISHED-BYTES GATE (verify-dist)\n------------------------------------------------------------------------\n========================================================================\nGLASS BOX DIST VERIFICATION \u2014 published bytes\n========================================================================\ndirectory : .\\frontend\\dist\ntext files : 176 scanned\nbinary files : 12 skipped (not text-scannable)\n assets/fraunces-latin-ext-standard-italic-CGbN9UgK.woff2, assets/fraunces-latin-ext-standard-normal-CJcjJNj7.woff2, assets/fraunces-latin-standard-italic-lSdLDfvT.woff2, assets/fraunces-latin-standard-normal-DihXLNYH.woff2, assets/fraunces-vietnamese-standard-italic-DxWqP7Ku.woff2, assets/fraunces-vietnamese-standard-normal-Czevyj-6.woff2, assets/hanken-grotesk-cyrillic-ext-wght-normal-D_UHSL_T.woff2, assets/hanken-grotesk-latin-ext-wght-normal-Dg-wlmqe.woff2, assets/hanken-grotesk-latin-wght-normal-CaVRRdDk.woff2, assets/hanken-grotesk-vietnamese-wght-normal-CHiFlh_0.woff2, favicon.ico, og-image.png\n\nCONTENT COMPLETENESS (the site must contain what it claims to)\n------------------------------------------------------------------------\n manifest_present ok\n snapshot/manifest.json parsed; quantlab 1.0.0 @ 9910554\n endpoint_count ok\n 151 endpoints declared, listed, and present on disk\n manifest_freshness ok\n captured 2026-08-14T21:46:06.391123+00:00 \u2014 -0.0h old (limit 14d)\n overview_has_data ok\n 4 of 4 account(s) carry a non-null equity figure\n entry_bundle ok\n 1 entry script(s) referenced and present: index-17GVHUPC.js\n\n CONTENT PASS \u2014 5 assertion(s) satisfied.\n\n========================================================================\nGLASS BOX SNAPSHOT \u2014 SANITIZATION REPORT\n========================================================================\nfiles scanned : 176\nbytes scanned : 1,973,470\nverdict : PASS\n\nFORBIDDEN PATTERNS (zero matches required to pass)\n------------------------------------------------------------------------\n alpaca_account_id 0 ok\n email_address 0 ok\n apca_api_header 0 ok\n authorization_header 0 ok\n env_secret_prefix:ALPACA_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_TREND_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_TREND_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_VOLTARGET_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_VOLTARGET_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_TREND_API_KEY 0 ok\n env_secret_prefix:ALPACA_TREND_SECRET_KEY 0 ok\n env_secret_prefix:SMTP_PASS 0 ok\n env_secret_prefix:TIINGO_API_KEY 0 ok\n\nENV-SECRET CHECK\n------------------------------------------------------------------------\n .env read locally; 10 value(s) reduced to 8-char prefixes and searched.\n keys checked: ALPACA_API_KEY, ALPACA_CRYPTO_TREND_API_KEY, ALPACA_CRYPTO_TREND_SECRET_KEY, ALPACA_CRYPTO_VOLTARGET_API_KEY, ALPACA_CRYPTO_VOLTARGET_SECRET_KEY, ALPACA_SECRET_KEY, ALPACA_TREND_API_KEY, ALPACA_TREND_SECRET_KEY, SMTP_PASS, TIINGO_API_KEY\n keys EXCLUDED as non-secret: ALERT_EMAIL_TO, ALPACA_BASE_URL, SMTP_HOST, SMTP_PORT, SMTP_USER\n (public URLs and documented endpoints; see is_secret_bearing)\n (prefixes themselves are never printed or stored)\n\nREDACTIONS PERFORMED (0)\n------------------------------------------------------------------------\n (none)\n\n========================================================================\nPASS \u2014 no forbidden pattern matched. Snapshot may be written.\n========================================================================\n\nREDACTABLE CONTENT IN PUBLISHED BYTES (0)\n------------------------------------------------------------------------\n (none \u2014 no local path leaked into the build)\n\n========================================================================\nDIST PASS \u2014 safe to deploy, subject to human review of this report.\n========================================================================\n\n========================================================================\nDRY RUN \u2014 gates passed, deploy withheld\n========================================================================", + "level": "INFO" + }, + { + "at": "2026-08-14T21:30:05.458157Z", + "kind": "alert", + "label": null, + "title": "glass box refresh ABORTED at 'snapshot' \u2014 human review needed", + "detail": "========================================================================\nGLASS BOX REFRESH \u2014 CHAIN REPORT\n========================================================================\nstarted : 2026-08-14T21:30:03.445299+00:00\nmode : LIVE\nsite : monzonautomation-glassbox (be63f48c-4949-4603-b8dd-a6ccfdd996e7)\n\nCHAIN (each step runs only if every earlier step passed)\n------------------------------------------------------------------------\n snapshot FAIL PermissionError: [WinError 5] Access is denied: 'reports'\n build SKIPPED \n verify-dist SKIPPED \n deploy SKIPPED \n\nPROVENANCE\n------------------------------------------------------------------------\n repo state : clean (main == origin/main)\n\nDEPLOY DECISION\n------------------------------------------------------------------------\n forbidden matches : 0 ok\n redactions : 0 ok\n redactable found : 0 ok\n NOT DEPLOYED \u2014 aborted at 'snapshot': PermissionError: [WinError 5] Access is denied: 'reports'\n\n========================================================================\nABORTED at 'snapshot'\n========================================================================", + "level": "WARNING" + }, + { + "at": "2026-08-14T21:00:02.932089Z", + "kind": "weekly_verdict", + "label": "voltarget", + "title": "week 2026-08-14: voltarget TRACKING", + "detail": "divergence +17 bps", + "level": null + }, + { + "at": "2026-08-14T21:00:02.932089Z", + "kind": "weekly_verdict", + "label": "trend", + "title": "week 2026-08-14: trend TRACKING", + "detail": "divergence +28 bps", + "level": null + }, + { + "at": "2026-08-14T21:00:02.932089Z", + "kind": "weekly_verdict", + "label": "crypto_trend", + "title": "week 2026-08-14: crypto_trend TRACKING", + "detail": "divergence +0 bps", + "level": null + }, + { + "at": "2026-08-14T21:00:02.932089Z", + "kind": "weekly_verdict", + "label": "crypto_voltarget", + "title": "week 2026-08-14: crypto_voltarget TRACKING", + "detail": "divergence -34 bps", + "level": null + }, + { + "at": "2026-08-14T06:38:15.450994Z", + "kind": "alert", + "label": "crypto_voltarget", + "title": "paper crypto_voltarget: 1 order(s) submitted, $3,238.06 notional", + "detail": "weights={'BTC-USD': 0.8939608815069376}; turnover=0.0329", + "level": "INFO" + }, + { + "at": "2026-08-14T06:38:15.042685Z", + "kind": "order", + "label": "crypto_voltarget", + "title": "buy BTC-USD $3,238.06", + "detail": "status=pending_new run=run_crypto_voltarget_20260814T063813Z", + "level": null + }, + { + "at": "2026-08-13T01:27:46.334893Z", + "kind": "alert", + "label": "crypto_voltarget", + "title": "paper crypto_voltarget: 1 order(s) submitted, $3,236.26 notional", + "detail": "weights={'BTC-USD': 0.8620658490902996}; turnover=0.0328", + "level": "INFO" + }, + { + "at": "2026-08-13T01:27:46.057365Z", + "kind": "order", + "label": "crypto_voltarget", + "title": "buy BTC-USD $3,236.26", + "detail": "status=pending_new run=run_crypto_voltarget_20260813T012744Z", + "level": null + }, + { + "at": "2026-08-11T14:00:08.369825Z", + "kind": "alert", + "label": "voltarget", + "title": "paper voltarget: 1 order(s) submitted, $1,487.39 notional", + "detail": "weights={'SPY': 0.7140967253638115}; turnover=0.0145", + "level": "INFO" + }, + { + "at": "2026-08-11T14:00:05.119384Z", + "kind": "order", + "label": "voltarget", + "title": "buy SPY $1,487.39", + "detail": "status=pending_new run=run_voltarget_20260811T140005Z", + "level": null + }, + { + "at": "2026-08-11T11:17:37.422386Z", + "kind": "alert", + "label": "crypto_voltarget", + "title": "paper crypto_voltarget: 1 order(s) submitted, $4,430.46 notional", + "detail": "weights={'BTC-USD': 0.8323567343776824}; turnover=0.0443", + "level": "INFO" + }, + { + "at": "2026-08-11T11:17:34.415849Z", + "kind": "order", + "label": "crypto_voltarget", + "title": "buy BTC-USD $4,430.46", + "detail": "status=pending_new run=run_crypto_voltarget_20260811T111735Z", + "level": null + }, + { + "at": "2026-08-10T22:58:22.027818Z", + "kind": "alert", + "label": null, + "title": "glass box refresh dry run \u2014 gates passed, not deployed", + "detail": "========================================================================\nGLASS BOX REFRESH \u2014 CHAIN REPORT\n========================================================================\nstarted : 2026-08-10T22:58:10.488175+00:00\nmode : DRY RUN (stops before deploy)\nsite : monzonautomation-glassbox (be63f48c-4949-4603-b8dd-a6ccfdd996e7)\n\nCHAIN (each step runs only if every earlier step passed)\n------------------------------------------------------------------------\n snapshot PASS 135 endpoint(s) captured, 136 file(s) written, 0 redaction(s)\n build PASS public bundle built into frontend/dist\n verify-dist PASS 160 published file(s) scanned, 0 forbidden, 0 redactable finding(s)\n deploy SKIPPED withheld (--dry-run); all gates passed\n\nDEPLOY DECISION\n------------------------------------------------------------------------\n forbidden matches : 0 ok\n redactions : 0 ok\n redactable found : 0 ok\n NOT DEPLOYED \u2014 dry run stopped before the deploy step\n\nSNAPSHOT SANITIZATION REPORT\n------------------------------------------------------------------------\n========================================================================\nGLASS BOX SNAPSHOT \u2014 SANITIZATION REPORT\n========================================================================\nfiles scanned : 136\nbytes scanned : 1,067,373\nverdict : PASS\n\nFORBIDDEN PATTERNS (zero matches required to pass)\n------------------------------------------------------------------------\n alpaca_account_id 0 ok\n email_address 0 ok\n apca_api_header 0 ok\n authorization_header 0 ok\n env_secret_prefix:ALPACA_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_TREND_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_TREND_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_VOLTARGET_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_VOLTARGET_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_TREND_API_KEY 0 ok\n env_secret_prefix:ALPACA_TREND_SECRET_KEY 0 ok\n env_secret_prefix:SMTP_PASS 0 ok\n env_secret_prefix:TIINGO_API_KEY 0 ok\n\nENV-SECRET CHECK\n------------------------------------------------------------------------\n .env read locally; 10 value(s) reduced to 8-char prefixes and searched.\n keys checked: ALPACA_API_KEY, ALPACA_CRYPTO_TREND_API_KEY, ALPACA_CRYPTO_TREND_SECRET_KEY, ALPACA_CRYPTO_VOLTARGET_API_KEY, ALPACA_CRYPTO_VOLTARGET_SECRET_KEY, ALPACA_SECRET_KEY, ALPACA_TREND_API_KEY, ALPACA_TREND_SECRET_KEY, SMTP_PASS, TIINGO_API_KEY\n keys EXCLUDED as non-secret: ALERT_EMAIL_TO, ALPACA_BASE_URL, SMTP_HOST, SMTP_PORT, SMTP_USER\n (public URLs and documented endpoints; see is_secret_bearing)\n (prefixes themselves are never printed or stored)\n\nREDACTIONS PERFORMED (0)\n------------------------------------------------------------------------\n (none)\n\n========================================================================\nPASS \u2014 no forbidden pattern matched. Snapshot may be written.\n========================================================================\n\nPUBLISHED-BYTES GATE (verify-dist)\n------------------------------------------------------------------------\n========================================================================\nGLASS BOX DIST VERIFICATION \u2014 published bytes\n========================================================================\ndirectory : .\\frontend\\dist\ntext files : 160 scanned\nbinary files : 12 skipped (not text-scannable)\n assets/fraunces-latin-ext-standard-italic-CGbN9UgK.woff2, assets/fraunces-latin-ext-standard-normal-CJcjJNj7.woff2, assets/fraunces-latin-standard-italic-lSdLDfvT.woff2, assets/fraunces-latin-standard-normal-DihXLNYH.woff2, assets/fraunces-vietnamese-standard-italic-DxWqP7Ku.woff2, assets/fraunces-vietnamese-standard-normal-Czevyj-6.woff2, assets/hanken-grotesk-cyrillic-ext-wght-normal-D_UHSL_T.woff2, assets/hanken-grotesk-latin-ext-wght-normal-Dg-wlmqe.woff2, assets/hanken-grotesk-latin-wght-normal-CaVRRdDk.woff2, assets/hanken-grotesk-vietnamese-wght-normal-CHiFlh_0.woff2, favicon.ico, og-image.png\n\nCONTENT COMPLETENESS (the site must contain what it claims to)\n------------------------------------------------------------------------\n manifest_present ok\n snapshot/manifest.json parsed; quantlab 1.0.0 @ 8a7f6ed\n endpoint_count ok\n 135 endpoints declared, listed, and present on disk\n manifest_freshness ok\n captured 2026-08-10T22:58:11.595175+00:00 \u2014 -0.0h old (limit 14d)\n overview_has_data ok\n 4 of 4 account(s) carry a non-null equity figure\n entry_bundle ok\n 1 entry script(s) referenced and present: index-17GVHUPC.js\n\n CONTENT PASS \u2014 5 assertion(s) satisfied.\n\n========================================================================\nGLASS BOX SNAPSHOT \u2014 SANITIZATION REPORT\n========================================================================\nfiles scanned : 160\nbytes scanned : 1,815,723\nverdict : PASS\n\nFORBIDDEN PATTERNS (zero matches required to pass)\n------------------------------------------------------------------------\n alpaca_account_id 0 ok\n email_address 0 ok\n apca_api_header 0 ok\n authorization_header 0 ok\n env_secret_prefix:ALPACA_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_TREND_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_TREND_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_VOLTARGET_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_VOLTARGET_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_TREND_API_KEY 0 ok\n env_secret_prefix:ALPACA_TREND_SECRET_KEY 0 ok\n env_secret_prefix:SMTP_PASS 0 ok\n env_secret_prefix:TIINGO_API_KEY 0 ok\n\nENV-SECRET CHECK\n------------------------------------------------------------------------\n .env read locally; 10 value(s) reduced to 8-char prefixes and searched.\n keys checked: ALPACA_API_KEY, ALPACA_CRYPTO_TREND_API_KEY, ALPACA_CRYPTO_TREND_SECRET_KEY, ALPACA_CRYPTO_VOLTARGET_API_KEY, ALPACA_CRYPTO_VOLTARGET_SECRET_KEY, ALPACA_SECRET_KEY, ALPACA_TREND_API_KEY, ALPACA_TREND_SECRET_KEY, SMTP_PASS, TIINGO_API_KEY\n keys EXCLUDED as non-secret: ALERT_EMAIL_TO, ALPACA_BASE_URL, SMTP_HOST, SMTP_PORT, SMTP_USER\n (public URLs and documented endpoints; see is_secret_bearing)\n (prefixes themselves are never printed or stored)\n\nREDACTIONS PERFORMED (0)\n------------------------------------------------------------------------\n (none)\n\n========================================================================\nPASS \u2014 no forbidden pattern matched. Snapshot may be written.\n========================================================================\n\nREDACTABLE CONTENT IN PUBLISHED BYTES (0)\n------------------------------------------------------------------------\n (none \u2014 no local path leaked into the build)\n\n========================================================================\nDIST PASS \u2014 safe to deploy, subject to human review of this report.\n========================================================================\n\n========================================================================\nDRY RUN \u2014 gates passed, deploy withheld\n========================================================================", + "level": "INFO" + }, { "at": "2026-08-10T22:56:54.081839Z", "kind": "alert", @@ -25,6 +185,54 @@ "detail": "========================================================================\nGLASS BOX REFRESH \u2014 CHAIN REPORT\n========================================================================\nstarted : 2026-08-10T22:51:37.247288+00:00\nmode : DRY RUN (stops before deploy)\nsite : monzonautomation-glassbox (be63f48c-4949-4603-b8dd-a6ccfdd996e7)\n\nCHAIN (each step runs only if every earlier step passed)\n------------------------------------------------------------------------\n snapshot PASS 135 endpoint(s) captured, 136 file(s) written, 0 redaction(s)\n build PASS public bundle built into frontend/dist\n verify-dist PASS 160 published file(s) scanned, 0 forbidden, 0 redactable finding(s)\n deploy SKIPPED withheld (--dry-run); all gates passed\n\nDEPLOY DECISION\n------------------------------------------------------------------------\n forbidden matches : 0 ok\n redactions : 0 ok\n redactable found : 0 ok\n NOT DEPLOYED \u2014 dry run stopped before the deploy step\n\nSNAPSHOT SANITIZATION REPORT\n------------------------------------------------------------------------\n========================================================================\nGLASS BOX SNAPSHOT \u2014 SANITIZATION REPORT\n========================================================================\nfiles scanned : 136\nbytes scanned : 1,042,657\nverdict : PASS\n\nFORBIDDEN PATTERNS (zero matches required to pass)\n------------------------------------------------------------------------\n alpaca_account_id 0 ok\n email_address 0 ok\n apca_api_header 0 ok\n authorization_header 0 ok\n env_secret_prefix:ALPACA_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_TREND_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_TREND_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_VOLTARGET_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_VOLTARGET_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_TREND_API_KEY 0 ok\n env_secret_prefix:ALPACA_TREND_SECRET_KEY 0 ok\n env_secret_prefix:SMTP_PASS 0 ok\n env_secret_prefix:TIINGO_API_KEY 0 ok\n\nENV-SECRET CHECK\n------------------------------------------------------------------------\n .env read locally; 10 value(s) reduced to 8-char prefixes and searched.\n keys checked: ALPACA_API_KEY, ALPACA_CRYPTO_TREND_API_KEY, ALPACA_CRYPTO_TREND_SECRET_KEY, ALPACA_CRYPTO_VOLTARGET_API_KEY, ALPACA_CRYPTO_VOLTARGET_SECRET_KEY, ALPACA_SECRET_KEY, ALPACA_TREND_API_KEY, ALPACA_TREND_SECRET_KEY, SMTP_PASS, TIINGO_API_KEY\n keys EXCLUDED as non-secret: ALERT_EMAIL_TO, ALPACA_BASE_URL, SMTP_HOST, SMTP_PORT, SMTP_USER\n (public URLs and documented endpoints; see is_secret_bearing)\n (prefixes themselves are never printed or stored)\n\nREDACTIONS PERFORMED (0)\n------------------------------------------------------------------------\n (none)\n\n========================================================================\nPASS \u2014 no forbidden pattern matched. Snapshot may be written.\n========================================================================\n\nPUBLISHED-BYTES GATE (verify-dist)\n------------------------------------------------------------------------\n========================================================================\nGLASS BOX DIST VERIFICATION \u2014 published bytes\n========================================================================\ndirectory : .\\frontend\\dist\ntext files : 160 scanned\nbinary files : 12 skipped (not text-scannable)\n assets/fraunces-latin-ext-standard-italic-CGbN9UgK.woff2, assets/fraunces-latin-ext-standard-normal-CJcjJNj7.woff2, assets/fraunces-latin-standard-italic-lSdLDfvT.woff2, assets/fraunces-latin-standard-normal-DihXLNYH.woff2, assets/fraunces-vietnamese-standard-italic-DxWqP7Ku.woff2, assets/fraunces-vietnamese-standard-normal-Czevyj-6.woff2, assets/hanken-grotesk-cyrillic-ext-wght-normal-D_UHSL_T.woff2, assets/hanken-grotesk-latin-ext-wght-normal-Dg-wlmqe.woff2, assets/hanken-grotesk-latin-wght-normal-CaVRRdDk.woff2, assets/hanken-grotesk-vietnamese-wght-normal-CHiFlh_0.woff2, favicon.ico, og-image.png\n\nCONTENT COMPLETENESS (the site must contain what it claims to)\n------------------------------------------------------------------------\n manifest_present ok\n snapshot/manifest.json parsed; quantlab 1.0.0 @ 8a7f6ed\n endpoint_count ok\n 135 endpoints declared, listed, and present on disk\n manifest_freshness ok\n captured 2026-08-10T22:51:37.895309+00:00 \u2014 -0.0h old (limit 14d)\n overview_has_data ok\n 4 of 4 account(s) carry a non-null equity figure\n entry_bundle ok\n 1 entry script(s) referenced and present: index-17GVHUPC.js\n\n CONTENT PASS \u2014 5 assertion(s) satisfied.\n\n========================================================================\nGLASS BOX SNAPSHOT \u2014 SANITIZATION REPORT\n========================================================================\nfiles scanned : 160\nbytes scanned : 1,790,983\nverdict : PASS\n\nFORBIDDEN PATTERNS (zero matches required to pass)\n------------------------------------------------------------------------\n alpaca_account_id 0 ok\n email_address 0 ok\n apca_api_header 0 ok\n authorization_header 0 ok\n env_secret_prefix:ALPACA_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_TREND_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_TREND_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_VOLTARGET_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_VOLTARGET_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_TREND_API_KEY 0 ok\n env_secret_prefix:ALPACA_TREND_SECRET_KEY 0 ok\n env_secret_prefix:SMTP_PASS 0 ok\n env_secret_prefix:TIINGO_API_KEY 0 ok\n\nENV-SECRET CHECK\n------------------------------------------------------------------------\n .env read locally; 10 value(s) reduced to 8-char prefixes and searched.\n keys checked: ALPACA_API_KEY, ALPACA_CRYPTO_TREND_API_KEY, ALPACA_CRYPTO_TREND_SECRET_KEY, ALPACA_CRYPTO_VOLTARGET_API_KEY, ALPACA_CRYPTO_VOLTARGET_SECRET_KEY, ALPACA_SECRET_KEY, ALPACA_TREND_API_KEY, ALPACA_TREND_SECRET_KEY, SMTP_PASS, TIINGO_API_KEY\n keys EXCLUDED as non-secret: ALERT_EMAIL_TO, ALPACA_BASE_URL, SMTP_HOST, SMTP_PORT, SMTP_USER\n (public URLs and documented endpoints; see is_secret_bearing)\n (prefixes themselves are never printed or stored)\n\nREDACTIONS PERFORMED (0)\n------------------------------------------------------------------------\n (none)\n\n========================================================================\nPASS \u2014 no forbidden pattern matched. Snapshot may be written.\n========================================================================\n\nREDACTABLE CONTENT IN PUBLISHED BYTES (0)\n------------------------------------------------------------------------\n (none \u2014 no local path leaked into the build)\n\n========================================================================\nDIST PASS \u2014 safe to deploy, subject to human review of this report.\n========================================================================\n\n========================================================================\nDRY RUN \u2014 gates passed, deploy withheld\n========================================================================", "level": "INFO" }, + { + "at": "2026-08-10T00:00:00Z", + "kind": "decision", + "label": null, + "title": "One bounded retry, and the list of things that must never get one", + "detail": "docs/decisions.md", + "level": null + }, + { + "at": "2026-08-10T00:00:00Z", + "kind": "decision", + "label": null, + "title": "The watchdog: making silence audible", + "detail": "docs/decisions.md", + "level": null + }, + { + "at": "2026-08-10T00:00:00Z", + "kind": "decision", + "label": null, + "title": "CI is healthy, and a provenance warning that does not block", + "detail": "docs/decisions.md", + "level": null + }, + { + "at": "2026-08-10T00:00:00Z", + "kind": "decision", + "label": null, + "title": "Automated Glass Box refresh, and the doctrine that lets a machine deploy", + "detail": "docs/decisions.md", + "level": null + }, + { + "at": "2026-08-10T00:00:00Z", + "kind": "decision", + "label": null, + "title": "The share card could not spell \"quantlab\"", + "detail": "docs/decisions.md", + "level": null + }, + { + "at": "2026-08-10T00:00:00Z", + "kind": "decision", + "label": null, + "title": "Local scheduling is a reliability floor, and a VPS is the real fix", + "detail": "docs/decisions.md", + "level": null + }, { "at": "2026-08-10T00:00:00Z", "kind": "decision", diff --git a/frontend/public/snapshot/manifest.json b/frontend/public/snapshot/manifest.json index d6c397a..7e57a45 100644 --- a/frontend/public/snapshot/manifest.json +++ b/frontend/public/snapshot/manifest.json @@ -1,6 +1,6 @@ { - "generated_at": "2026-08-10T22:58:11.595175Z", - "git_commit": "8a7f6ed", + "generated_at": "2026-08-15T16:57:39.748655Z", + "git_commit": "9910554", "quantlab_version": "1.0.0", "endpoints": [ { @@ -9,7 +9,7 @@ "path": "/api/decisions", "params": {}, "status": 200, - "bytes": 80434 + "bytes": 112280 }, { "key": "/api/divergence", @@ -17,7 +17,7 @@ "path": "/api/divergence", "params": {}, "status": 200, - "bytes": 20104 + "bytes": 24906 }, { "key": "/api/divergence?label=voltarget", @@ -27,7 +27,7 @@ "label": "voltarget" }, "status": 200, - "bytes": 4204 + "bytes": 5017 }, { "key": "/api/divergence?label=trend", @@ -37,7 +37,7 @@ "label": "trend" }, "status": 200, - "bytes": 4873 + "bytes": 5683 }, { "key": "/api/divergence?label=crypto_trend", @@ -47,7 +47,7 @@ "label": "crypto_trend" }, "status": 200, - "bytes": 5155 + "bytes": 6710 }, { "key": "/api/divergence?label=crypto_voltarget", @@ -57,7 +57,7 @@ "label": "crypto_voltarget" }, "status": 200, - "bytes": 6127 + "bytes": 7751 }, { "key": "/api/equity", @@ -65,7 +65,7 @@ "path": "/api/equity", "params": {}, "status": 200, - "bytes": 35167 + "bytes": 40500 }, { "key": "/api/equity?label=voltarget", @@ -75,7 +75,7 @@ "label": "voltarget" }, "status": 200, - "bytes": 6434 + "bytes": 7426 }, { "key": "/api/equity?label=trend", @@ -85,7 +85,7 @@ "label": "trend" }, "status": 200, - "bytes": 6052 + "bytes": 7044 }, { "key": "/api/equity?label=crypto_trend", @@ -95,7 +95,7 @@ "label": "crypto_trend" }, "status": 200, - "bytes": 11426 + "bytes": 13100 }, { "key": "/api/equity?label=crypto_voltarget", @@ -105,7 +105,7 @@ "label": "crypto_voltarget" }, "status": 200, - "bytes": 11448 + "bytes": 13123 }, { "key": "/api/ignored-inputs", @@ -121,7 +121,7 @@ "path": "/api/overview", "params": {}, "status": 200, - "bytes": 5514 + "bytes": 5314 }, { "key": "/api/risk", @@ -137,7 +137,7 @@ "path": "/api/runs", "params": {}, "status": 200, - "bytes": 207722 + "bytes": 238397 }, { "key": "/api/runs?label=voltarget", @@ -147,7 +147,7 @@ "label": "voltarget" }, "status": 200, - "bytes": 47025 + "bytes": 53946 }, { "key": "/api/runs?label=trend", @@ -157,7 +157,7 @@ "label": "trend" }, "status": 200, - "bytes": 33287 + "bytes": 39422 }, { "key": "/api/runs?label=crypto_trend", @@ -167,7 +167,7 @@ "label": "crypto_trend" }, "status": 200, - "bytes": 54240 + "bytes": 61760 }, { "key": "/api/runs?label=crypto_voltarget", @@ -177,7 +177,151 @@ "label": "crypto_voltarget" }, "status": 200, - "bytes": 73401 + "bytes": 83500 + }, + { + "key": "/api/runs/run_crypto_voltarget_20260815T003008Z/narrate", + "file": "api-runs-run_crypto_voltarget_20260815T003008Z-narrate.json", + "path": "/api/runs/run_crypto_voltarget_20260815T003008Z/narrate", + "params": {}, + "status": 200, + "bytes": 2862 + }, + { + "key": "/api/runs/run_crypto_trend_20260815T003005Z/narrate", + "file": "api-runs-run_crypto_trend_20260815T003005Z-narrate.json", + "path": "/api/runs/run_crypto_trend_20260815T003005Z/narrate", + "params": {}, + "status": 200, + "bytes": 1636 + }, + { + "key": "/api/runs/run_trend_20260814T140007Z/narrate", + "file": "api-runs-run_trend_20260814T140007Z-narrate.json", + "path": "/api/runs/run_trend_20260814T140007Z/narrate", + "params": {}, + "status": 200, + "bytes": 2294 + }, + { + "key": "/api/runs/run_voltarget_20260814T140004Z/narrate", + "file": "api-runs-run_voltarget_20260814T140004Z-narrate.json", + "path": "/api/runs/run_voltarget_20260814T140004Z/narrate", + "params": {}, + "status": 200, + "bytes": 2781 + }, + { + "key": "/api/runs/run_crypto_voltarget_20260814T063813Z/narrate", + "file": "api-runs-run_crypto_voltarget_20260814T063813Z-narrate.json", + "path": "/api/runs/run_crypto_voltarget_20260814T063813Z/narrate", + "params": {}, + "status": 200, + "bytes": 3530 + }, + { + "key": "/api/runs/run_crypto_trend_20260814T063810Z/narrate", + "file": "api-runs-run_crypto_trend_20260814T063810Z-narrate.json", + "path": "/api/runs/run_crypto_trend_20260814T063810Z/narrate", + "params": {}, + "status": 200, + "bytes": 1636 + }, + { + "key": "/api/runs/run_trend_20260813T140007Z/narrate", + "file": "api-runs-run_trend_20260813T140007Z-narrate.json", + "path": "/api/runs/run_trend_20260813T140007Z/narrate", + "params": {}, + "status": 200, + "bytes": 2294 + }, + { + "key": "/api/runs/run_voltarget_20260813T140004Z/narrate", + "file": "api-runs-run_voltarget_20260813T140004Z-narrate.json", + "path": "/api/runs/run_voltarget_20260813T140004Z/narrate", + "params": {}, + "status": 200, + "bytes": 2780 + }, + { + "key": "/api/runs/run_crypto_voltarget_20260813T012744Z/narrate", + "file": "api-runs-run_crypto_voltarget_20260813T012744Z-narrate.json", + "path": "/api/runs/run_crypto_voltarget_20260813T012744Z/narrate", + "params": {}, + "status": 200, + "bytes": 3527 + }, + { + "key": "/api/runs/run_crypto_trend_20260813T012741Z/narrate", + "file": "api-runs-run_crypto_trend_20260813T012741Z-narrate.json", + "path": "/api/runs/run_crypto_trend_20260813T012741Z/narrate", + "params": {}, + "status": 200, + "bytes": 1636 + }, + { + "key": "/api/runs/run_trend_20260812T140032Z/narrate", + "file": "api-runs-run_trend_20260812T140032Z-narrate.json", + "path": "/api/runs/run_trend_20260812T140032Z/narrate", + "params": {}, + "status": 200, + "bytes": 2294 + }, + { + "key": "/api/runs/run_voltarget_20260812T140025Z/narrate", + "file": "api-runs-run_voltarget_20260812T140025Z-narrate.json", + "path": "/api/runs/run_voltarget_20260812T140025Z/narrate", + "params": {}, + "status": 200, + "bytes": 2780 + }, + { + "key": "/api/runs/run_crypto_voltarget_20260812T123722Z/narrate", + "file": "api-runs-run_crypto_voltarget_20260812T123722Z-narrate.json", + "path": "/api/runs/run_crypto_voltarget_20260812T123722Z/narrate", + "params": {}, + "status": 200, + "bytes": 2862 + }, + { + "key": "/api/runs/run_crypto_trend_20260812T123719Z/narrate", + "file": "api-runs-run_crypto_trend_20260812T123719Z-narrate.json", + "path": "/api/runs/run_crypto_trend_20260812T123719Z/narrate", + "params": {}, + "status": 200, + "bytes": 1636 + }, + { + "key": "/api/runs/run_trend_20260811T140012Z/narrate", + "file": "api-runs-run_trend_20260811T140012Z-narrate.json", + "path": "/api/runs/run_trend_20260811T140012Z/narrate", + "params": {}, + "status": 200, + "bytes": 2295 + }, + { + "key": "/api/runs/run_voltarget_20260811T140005Z/narrate", + "file": "api-runs-run_voltarget_20260811T140005Z-narrate.json", + "path": "/api/runs/run_voltarget_20260811T140005Z/narrate", + "params": {}, + "status": 200, + "bytes": 3439 + }, + { + "key": "/api/runs/run_crypto_voltarget_20260811T111735Z/narrate", + "file": "api-runs-run_crypto_voltarget_20260811T111735Z-narrate.json", + "path": "/api/runs/run_crypto_voltarget_20260811T111735Z/narrate", + "params": {}, + "status": 200, + "bytes": 3532 + }, + { + "key": "/api/runs/run_crypto_trend_20260811T111731Z/narrate", + "file": "api-runs-run_crypto_trend_20260811T111731Z-narrate.json", + "path": "/api/runs/run_crypto_trend_20260811T111731Z/narrate", + "params": {}, + "status": 200, + "bytes": 1636 }, { "key": "/api/runs/run_trend_20260810T140010Z/narrate", @@ -1105,9 +1249,9 @@ "path": "/api/timeline", "params": {}, "status": 200, - "bytes": 112346 + "bytes": 153468 } ], - "endpoint_count": 135, + "endpoint_count": 153, "note": "Static capture of a localhost-only read-only API. Every figure was read from an artifact this system wrote. Refreshed manually." } \ No newline at end of file From 2c51d991a6b0821d54e9f74667562d4f2378c90b Mon Sep 17 00:00:00 2001 From: danielfmonzon <123423019+danielfmonzon@users.noreply.github.com> Date: Sat, 15 Aug 2026 15:15:15 -0400 Subject: [PATCH 3/3] snapshot: captures from the post-merge refresh The 18:01Z chain regenerated these after PROP-1 merged; the Story page copy and the manifest commit both moved. --- frontend/public/snapshot/api-overview.json | 2 +- frontend/public/snapshot/api-timeline.json | 10 +++++++++- frontend/public/snapshot/manifest.json | 6 +++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/frontend/public/snapshot/api-overview.json b/frontend/public/snapshot/api-overview.json index 9b2512a..372c2fc 100644 --- a/frontend/public/snapshot/api-overview.json +++ b/frontend/public/snapshot/api-overview.json @@ -1,5 +1,5 @@ { - "generated_at": "2026-08-15T16:57:39.301667Z", + "generated_at": "2026-08-15T18:01:17.129403Z", "accounts": [ { "label": "voltarget", diff --git a/frontend/public/snapshot/api-timeline.json b/frontend/public/snapshot/api-timeline.json index bff4a05..b82cee0 100644 --- a/frontend/public/snapshot/api-timeline.json +++ b/frontend/public/snapshot/api-timeline.json @@ -1,6 +1,14 @@ { - "count": 378, + "count": 379, "events": [ + { + "at": "2026-08-15T16:57:49.716238Z", + "kind": "alert", + "label": null, + "title": "glass box refresh dry run \u2014 gates passed, not deployed", + "detail": "========================================================================\nGLASS BOX REFRESH \u2014 CHAIN REPORT\n========================================================================\nstarted : 2026-08-15T16:57:38.738472+00:00\nmode : DRY RUN (stops before deploy)\nsite : monzonautomation-glassbox (be63f48c-4949-4603-b8dd-a6ccfdd996e7)\n\nCHAIN (each step runs only if every earlier step passed)\n------------------------------------------------------------------------\n snapshot PASS 153 endpoint(s) captured, 154 file(s) written, 0 redaction(s)\n build PASS public bundle built into frontend/dist\n verify-dist PASS 178 published file(s) scanned, 0 forbidden, 0 redactable finding(s)\n deploy SKIPPED withheld (--dry-run); all gates passed\n\nPROVENANCE\n------------------------------------------------------------------------\n repo state : WARNING\n working tree is DIRTY: 49 path(s) differ from HEAD [docs/decisions.md, frontend/public/snapshot/api-decisions.json, frontend/public/snapshot/api-divergence--label-crypto_trend.json, frontend/public/snapshot/api-divergence--label-crypto_voltarget.json, frontend/public/snapshot/api-divergence--label-trend.json (+44 more)] \u2014 artifacts will record a commit that does not contain them\n\nDEPLOY DECISION\n------------------------------------------------------------------------\n forbidden matches : 0 ok\n redactions : 0 ok\n redactable found : 0 ok\n env-secret check : ran\n NOT DEPLOYED \u2014 dry run stopped before the deploy step\n\nSNAPSHOT SANITIZATION REPORT\n------------------------------------------------------------------------\n========================================================================\nGLASS BOX SNAPSHOT \u2014 SANITIZATION REPORT\n========================================================================\nfiles scanned : 154\nbytes scanned : 1,272,264\nverdict : PASS\n\nFORBIDDEN PATTERNS (zero matches required to pass)\n------------------------------------------------------------------------\n alpaca_account_id 0 ok\n email_address 0 ok\n apca_api_header 0 ok\n authorization_header 0 ok\n env_secret_prefix:ALPACA_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_TREND_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_TREND_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_VOLTARGET_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_VOLTARGET_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_TREND_API_KEY 0 ok\n env_secret_prefix:ALPACA_TREND_SECRET_KEY 0 ok\n env_secret_prefix:SMTP_PASS 0 ok\n env_secret_prefix:TIINGO_API_KEY 0 ok\n\nENV-SECRET CHECK\n------------------------------------------------------------------------\n .env read locally; 10 value(s) reduced to 8-char prefixes and searched.\n keys checked: ALPACA_API_KEY, ALPACA_CRYPTO_TREND_API_KEY, ALPACA_CRYPTO_TREND_SECRET_KEY, ALPACA_CRYPTO_VOLTARGET_API_KEY, ALPACA_CRYPTO_VOLTARGET_SECRET_KEY, ALPACA_SECRET_KEY, ALPACA_TREND_API_KEY, ALPACA_TREND_SECRET_KEY, SMTP_PASS, TIINGO_API_KEY\n keys EXCLUDED as non-secret: ALERT_EMAIL_TO, ALPACA_BASE_URL, SMTP_HOST, SMTP_PORT, SMTP_USER\n (public URLs and documented endpoints; see is_secret_bearing)\n (prefixes themselves are never printed or stored)\n\nREDACTIONS PERFORMED (0)\n------------------------------------------------------------------------\n (none)\n\n========================================================================\nPASS \u2014 no forbidden pattern matched. Snapshot may be written.\n========================================================================\n\nPUBLISHED-BYTES GATE (verify-dist)\n------------------------------------------------------------------------\n========================================================================\nGLASS BOX DIST VERIFICATION \u2014 published bytes\n========================================================================\ndirectory : .\\frontend\\dist\ntext files : 178 scanned\nbinary files : 12 skipped (not text-scannable)\n assets/fraunces-latin-ext-standard-italic-CGbN9UgK.woff2, assets/fraunces-latin-ext-standard-normal-CJcjJNj7.woff2, assets/fraunces-latin-standard-italic-lSdLDfvT.woff2, assets/fraunces-latin-standard-normal-DihXLNYH.woff2, assets/fraunces-vietnamese-standard-italic-DxWqP7Ku.woff2, assets/fraunces-vietnamese-standard-normal-Czevyj-6.woff2, assets/hanken-grotesk-cyrillic-ext-wght-normal-D_UHSL_T.woff2, assets/hanken-grotesk-latin-ext-wght-normal-Dg-wlmqe.woff2, assets/hanken-grotesk-latin-wght-normal-CaVRRdDk.woff2, assets/hanken-grotesk-vietnamese-wght-normal-CHiFlh_0.woff2, favicon.ico, og-image.png\n\nCONTENT COMPLETENESS (the site must contain what it claims to)\n------------------------------------------------------------------------\n manifest_present ok\n snapshot/manifest.json parsed; quantlab 1.0.0 @ 9910554\n endpoint_count ok\n 153 endpoints declared, listed, and present on disk\n manifest_freshness ok\n captured 2026-08-15T16:57:39.748655+00:00 \u2014 -0.0h old (limit 14d)\n overview_has_data ok\n 4 of 4 account(s) carry a non-null equity figure\n entry_bundle ok\n 1 entry script(s) referenced and present: index-17GVHUPC.js\n\n CONTENT PASS \u2014 5 assertion(s) satisfied.\n\n========================================================================\nGLASS BOX SNAPSHOT \u2014 SANITIZATION REPORT\n========================================================================\nfiles scanned : 178\nbytes scanned : 2,024,739\nverdict : PASS\n\nFORBIDDEN PATTERNS (zero matches required to pass)\n------------------------------------------------------------------------\n alpaca_account_id 0 ok\n email_address 0 ok\n apca_api_header 0 ok\n authorization_header 0 ok\n env_secret_prefix:ALPACA_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_TREND_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_TREND_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_VOLTARGET_API_KEY 0 ok\n env_secret_prefix:ALPACA_CRYPTO_VOLTARGET_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_SECRET_KEY 0 ok\n env_secret_prefix:ALPACA_TREND_API_KEY 0 ok\n env_secret_prefix:ALPACA_TREND_SECRET_KEY 0 ok\n env_secret_prefix:SMTP_PASS 0 ok\n env_secret_prefix:TIINGO_API_KEY 0 ok\n\nENV-SECRET CHECK\n------------------------------------------------------------------------\n .env read locally; 10 value(s) reduced to 8-char prefixes and searched.\n keys checked: ALPACA_API_KEY, ALPACA_CRYPTO_TREND_API_KEY, ALPACA_CRYPTO_TREND_SECRET_KEY, ALPACA_CRYPTO_VOLTARGET_API_KEY, ALPACA_CRYPTO_VOLTARGET_SECRET_KEY, ALPACA_SECRET_KEY, ALPACA_TREND_API_KEY, ALPACA_TREND_SECRET_KEY, SMTP_PASS, TIINGO_API_KEY\n keys EXCLUDED as non-secret: ALERT_EMAIL_TO, ALPACA_BASE_URL, SMTP_HOST, SMTP_PORT, SMTP_USER\n (public URLs and documented endpoints; see is_secret_bearing)\n (prefixes themselves are never printed or stored)\n\nREDACTIONS PERFORMED (0)\n------------------------------------------------------------------------\n (none)\n\n========================================================================\nPASS \u2014 no forbidden pattern matched. Snapshot may be written.\n========================================================================\n\nREDACTABLE CONTENT IN PUBLISHED BYTES (0)\n------------------------------------------------------------------------\n (none \u2014 no local path leaked into the build)\n\n========================================================================\nDIST PASS \u2014 safe to deploy, subject to human review of this report.\n========================================================================\n\n========================================================================\nDRY RUN \u2014 gates passed, deploy withheld\n========================================================================", + "level": "INFO" + }, { "at": "2026-08-15T16:24:57.881659Z", "kind": "alert", diff --git a/frontend/public/snapshot/manifest.json b/frontend/public/snapshot/manifest.json index 7e57a45..54d1bb4 100644 --- a/frontend/public/snapshot/manifest.json +++ b/frontend/public/snapshot/manifest.json @@ -1,6 +1,6 @@ { - "generated_at": "2026-08-15T16:57:39.748655Z", - "git_commit": "9910554", + "generated_at": "2026-08-15T18:01:17.556656Z", + "git_commit": "8a192ad", "quantlab_version": "1.0.0", "endpoints": [ { @@ -1249,7 +1249,7 @@ "path": "/api/timeline", "params": {}, "status": 200, - "bytes": 153468 + "bytes": 162238 } ], "endpoint_count": 153,