Retire completed GTL tuning harnesses
This commit is contained in:
@@ -61,25 +61,10 @@ def _parse_args() -> argparse.Namespace:
|
||||
"rewrite_range504_structural_primary2",
|
||||
"production_structural_overlay",
|
||||
"explicit_target_ladder",
|
||||
"gtl_tuning",
|
||||
"gtl_confirmation",
|
||||
),
|
||||
default=None,
|
||||
help="Research-only S/R detector/gate arm.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--gtl-config",
|
||||
default=None,
|
||||
help="Research-only GTL configuration as a JSON object (requires --sr-variant gtl_tuning).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--gtl-confirm-config",
|
||||
default=None,
|
||||
help=(
|
||||
"Research-only GTL intersection/union configuration as JSON "
|
||||
"(requires --sr-variant gtl_confirmation)."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--entry-start",
|
||||
default=None,
|
||||
@@ -215,16 +200,6 @@ async def _main() -> None:
|
||||
os.environ["BACKTEST_ALLOW_SPAWN"] = "1"
|
||||
if args.sr_variant:
|
||||
os.environ["BACKTEST_SR_VARIANT"] = args.sr_variant
|
||||
if args.gtl_config:
|
||||
if args.sr_variant != "gtl_tuning":
|
||||
raise SystemExit("--gtl-config requires --sr-variant gtl_tuning")
|
||||
os.environ["BACKTEST_GTL_CONFIG"] = args.gtl_config
|
||||
if args.gtl_confirm_config:
|
||||
if args.sr_variant != "gtl_confirmation":
|
||||
raise SystemExit(
|
||||
"--gtl-confirm-config requires --sr-variant gtl_confirmation"
|
||||
)
|
||||
os.environ["BACKTEST_GTL_CONFIRM_CONFIG"] = args.gtl_confirm_config
|
||||
if args.entry_start:
|
||||
os.environ["BACKTEST_ENTRY_START"] = args.entry_start
|
||||
if args.entry_end:
|
||||
|
||||
@@ -1,360 +0,0 @@
|
||||
"""Run the evidence-selected GTL confirmation/union matrix with one command.
|
||||
|
||||
The first GTL tuning matrix tested replacements. This follow-up decomposes the
|
||||
four informative variants into retained-only intersections and control-plus-
|
||||
addition unions while preserving frozen control geometry wherever possible.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import date, datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from scripts import run_gtl_tuning_matrix as common # noqa: E402
|
||||
|
||||
RUNNER = ROOT / "scripts" / "run_backtest_snapshot.py"
|
||||
REFERENCE_MATRIX = ROOT / "reports" / "backtest-20260713-gtl-tuning-matrix.json"
|
||||
|
||||
TOUCH = {"name": "touch_0_25pct", "touch_tolerance": 0.0025}
|
||||
STRENGTH = {"name": "strength_1000", "strength_scale": 1000.0}
|
||||
MERGE = {"name": "merge_0_25pct", "merge_tolerance": 0.0025}
|
||||
GRID_ONLY = {"name": "pivots_none", "include_pivots": False}
|
||||
|
||||
|
||||
def _arm(
|
||||
name: str,
|
||||
description: str,
|
||||
mode: str,
|
||||
*confirmations: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"mode": mode,
|
||||
"confirmations": list(confirmations),
|
||||
}
|
||||
|
||||
|
||||
# Pre-registered from the replacement matrix's paired cohorts. Replacement
|
||||
# arms that plainly removed strong control setups or added weak cohorts are not
|
||||
# repeated here.
|
||||
GTL_CONFIRMATION_ARMS: tuple[dict[str, Any], ...] = (
|
||||
_arm("control", "Frozen explicit GTL; composition-path parity control.", "intersection"),
|
||||
_arm(
|
||||
"touch_intersection",
|
||||
"Retain control setups also qualified with 0.25% touch padding.",
|
||||
"intersection",
|
||||
TOUCH,
|
||||
),
|
||||
_arm(
|
||||
"touch_union",
|
||||
"Keep control and admit additions from 0.25% touch padding.",
|
||||
"union",
|
||||
TOUCH,
|
||||
),
|
||||
_arm(
|
||||
"strength_intersection",
|
||||
"Retain control setups also qualified at strength scale 1000.",
|
||||
"intersection",
|
||||
STRENGTH,
|
||||
),
|
||||
_arm(
|
||||
"strength_union",
|
||||
"Keep control and admit additions from strength scale 1000.",
|
||||
"union",
|
||||
STRENGTH,
|
||||
),
|
||||
_arm(
|
||||
"merge_intersection",
|
||||
"Retain control setups also qualified with 0.25% proposal merging.",
|
||||
"intersection",
|
||||
MERGE,
|
||||
),
|
||||
_arm(
|
||||
"merge_union",
|
||||
"Keep control and admit additions from 0.25% proposal merging.",
|
||||
"union",
|
||||
MERGE,
|
||||
),
|
||||
_arm(
|
||||
"grid_intersection",
|
||||
"Retain control setups also qualified by the range grid without pivots.",
|
||||
"intersection",
|
||||
GRID_ONLY,
|
||||
),
|
||||
_arm(
|
||||
"grid_union",
|
||||
"Keep control and admit additions from the range grid without pivots.",
|
||||
"union",
|
||||
GRID_ONLY,
|
||||
),
|
||||
_arm(
|
||||
"touch_strength_intersection",
|
||||
"Require both tighter-touch and faster-strength confirmation.",
|
||||
"intersection",
|
||||
TOUCH,
|
||||
STRENGTH,
|
||||
),
|
||||
_arm(
|
||||
"touch_merge_intersection",
|
||||
"Require both tighter-touch and tighter-merge confirmation.",
|
||||
"intersection",
|
||||
TOUCH,
|
||||
MERGE,
|
||||
),
|
||||
_arm(
|
||||
"strength_merge_intersection",
|
||||
"Require both faster-strength and tighter-merge confirmation.",
|
||||
"intersection",
|
||||
STRENGTH,
|
||||
MERGE,
|
||||
),
|
||||
_arm(
|
||||
"touch_strength_merge_intersection",
|
||||
"Require all three high-breadth confirmation variants.",
|
||||
"intersection",
|
||||
TOUCH,
|
||||
STRENGTH,
|
||||
MERGE,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"snapshot",
|
||||
nargs="?",
|
||||
default="backtest_snapshots/prod.sqlite",
|
||||
help="Local SQLite snapshot path.",
|
||||
)
|
||||
parser.add_argument("--workers", type=int, default=7)
|
||||
parser.add_argument("--holdout-split", default="2024-07-01")
|
||||
parser.add_argument(
|
||||
"--out",
|
||||
default=None,
|
||||
help=(
|
||||
"Consolidated JSON path. Defaults to "
|
||||
"reports/backtest-YYYYMMDD-gtl-confirmation-matrix.json."
|
||||
),
|
||||
)
|
||||
parser.add_argument("--keep-arm-reports", action="store_true")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _config(arm: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"name": arm["name"],
|
||||
"mode": arm["mode"],
|
||||
"confirmations": arm["confirmations"],
|
||||
}
|
||||
|
||||
|
||||
def _signature(arm: dict) -> dict:
|
||||
return {
|
||||
"candidates": arm.get("candidates"),
|
||||
"qualified": arm.get("qualified"),
|
||||
"qualified_net_avg_r": arm.get("qualified_net_avg_r"),
|
||||
"qualified_net_avg_r_ex_top5": arm.get("qualified_net_avg_r_ex_top5"),
|
||||
"full_book": arm.get("full_book"),
|
||||
"holdout": arm.get("holdout"),
|
||||
}
|
||||
|
||||
|
||||
def _reference_control() -> dict | None:
|
||||
if not REFERENCE_MATRIX.exists():
|
||||
return None
|
||||
with REFERENCE_MATRIX.open(encoding="utf-8") as handle:
|
||||
payload = json.load(handle)
|
||||
return next(
|
||||
(arm for arm in payload.get("arms") or [] if arm.get("name") == "control"),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def _write_markdown(path: Path, payload: dict) -> None:
|
||||
rows = [
|
||||
"# GTL confirmation/union matrix",
|
||||
"",
|
||||
f"Status: **{payload['status']}** ",
|
||||
f"Holdout split: `{payload['holdout_split']}` ",
|
||||
f"Completed arms: {len(payload['arms'])}/{payload['arm_count']}",
|
||||
"",
|
||||
"| Arm | Mode | Qualified | Full Sharpe | CAGR | Max DD | Trades | Train Sharpe | Test Sharpe | Ex-top-5% R | Screen |",
|
||||
"|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|",
|
||||
]
|
||||
for arm in payload["arms"]:
|
||||
full = arm.get("full_book") or {}
|
||||
holdout = arm.get("holdout") or {}
|
||||
screen = arm.get("screen") or {}
|
||||
rows.append(
|
||||
"| "
|
||||
+ " | ".join((
|
||||
arm["name"],
|
||||
arm["config"]["mode"],
|
||||
str(arm.get("qualified") or "-"),
|
||||
common._fmt(full.get("sharpe")),
|
||||
common._fmt(full.get("cagr_pct"), 1),
|
||||
common._fmt(full.get("max_drawdown_pct"), 1),
|
||||
str(full.get("trades") or "-"),
|
||||
common._fmt((holdout.get("train") or {}).get("sharpe")),
|
||||
common._fmt((holdout.get("test") or {}).get("sharpe")),
|
||||
common._fmt(arm.get("qualified_net_avg_r_ex_top5"), 3),
|
||||
f"{screen.get('passed', '-')}/{screen.get('total', '-')}",
|
||||
))
|
||||
+ " |"
|
||||
)
|
||||
rows.extend((
|
||||
"",
|
||||
"## Interpretation guardrail",
|
||||
"",
|
||||
"Intersections test the retained control cohort; unions test control plus genuinely added setups. The post-2024 interval is a robustness check, not a pristine holdout. Passing does not authorize deployment.",
|
||||
"",
|
||||
))
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("\n".join(rows), encoding="utf-8")
|
||||
|
||||
|
||||
def _run_arm(
|
||||
arm: dict[str, Any],
|
||||
snapshot: Path,
|
||||
workers: int,
|
||||
holdout_split: str,
|
||||
output: Path,
|
||||
) -> None:
|
||||
command = [
|
||||
sys.executable,
|
||||
str(RUNNER),
|
||||
str(snapshot),
|
||||
"--workers", str(workers),
|
||||
"--allow-spawn",
|
||||
"--sr-variant", "gtl_confirmation",
|
||||
"--gtl-confirm-config", json.dumps(_config(arm), separators=(",", ":")),
|
||||
"--holdout-split", holdout_split,
|
||||
"--sr-audit",
|
||||
"--out", str(output),
|
||||
]
|
||||
subprocess.run(command, cwd=ROOT, check=True)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = _args()
|
||||
snapshot = Path(args.snapshot).resolve()
|
||||
if not snapshot.exists():
|
||||
raise SystemExit(f"Snapshot not found: {snapshot}")
|
||||
if args.workers < 1:
|
||||
raise SystemExit("--workers must be at least 1")
|
||||
try:
|
||||
date.fromisoformat(args.holdout_split)
|
||||
except ValueError as exc:
|
||||
raise SystemExit("--holdout-split must use YYYY-MM-DD") from exc
|
||||
|
||||
stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
|
||||
default_out = ROOT / "reports" / f"backtest-{stamp[:8]}-gtl-confirmation-matrix.json"
|
||||
out_path = Path(args.out) if args.out else default_out
|
||||
if not out_path.is_absolute():
|
||||
out_path = ROOT / out_path
|
||||
markdown_path = out_path.with_suffix(".md")
|
||||
work_dir = ROOT / "reports" / f".gtl-confirmation-work-{stamp}"
|
||||
work_dir.mkdir(parents=True, exist_ok=False)
|
||||
|
||||
reference = _reference_control()
|
||||
payload: dict[str, Any] = {
|
||||
"status": "running",
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"snapshot": str(snapshot),
|
||||
"workers": args.workers,
|
||||
"holdout_split": args.holdout_split,
|
||||
"arm_count": len(GTL_CONFIRMATION_ARMS),
|
||||
"reference_matrix": str(REFERENCE_MATRIX) if reference else None,
|
||||
"arms": [],
|
||||
}
|
||||
common._write_json(out_path, payload)
|
||||
_write_markdown(markdown_path, payload)
|
||||
|
||||
control_report: dict | None = None
|
||||
arm_outputs: list[Path] = []
|
||||
try:
|
||||
for index, arm in enumerate(GTL_CONFIRMATION_ARMS, start=1):
|
||||
name = arm["name"]
|
||||
output = work_dir / f"{index:02d}-{name}.json"
|
||||
arm_outputs.append(output)
|
||||
print(f"\n[{index}/{len(GTL_CONFIRMATION_ARMS)}] {name}", flush=True)
|
||||
print(f" {arm['description']}", flush=True)
|
||||
_run_arm(arm, snapshot, args.workers, args.holdout_split, output)
|
||||
report = common._load_report(output)
|
||||
compact = common._compact_arm(report, _config(arm), control_report)
|
||||
compact["description"] = arm["description"]
|
||||
if control_report is None:
|
||||
control_report = report
|
||||
compact["screen"] = {
|
||||
"checks": {}, "passed": 0, "total": 0, "advances": False,
|
||||
}
|
||||
if reference is not None and _signature(compact) != _signature(reference):
|
||||
raise RuntimeError(
|
||||
"Confirmation-path control does not reproduce the frozen GTL matrix control"
|
||||
)
|
||||
payload["control_parity"] = "pass" if reference is not None else "not_checked"
|
||||
else:
|
||||
compact["screen"] = common._screen_arm(compact, payload["arms"][0])
|
||||
payload["arms"].append(compact)
|
||||
payload["completed_at"] = datetime.now(timezone.utc).isoformat()
|
||||
common._write_json(out_path, payload)
|
||||
_write_markdown(markdown_path, payload)
|
||||
except BaseException as exc:
|
||||
payload["status"] = "failed"
|
||||
payload["failed_at"] = datetime.now(timezone.utc).isoformat()
|
||||
payload["error"] = f"{type(exc).__name__}: {exc}"
|
||||
payload["work_dir"] = str(work_dir)
|
||||
common._write_json(out_path, payload)
|
||||
_write_markdown(markdown_path, payload)
|
||||
raise
|
||||
|
||||
payload["status"] = "complete"
|
||||
payload["completed_at"] = datetime.now(timezone.utc).isoformat()
|
||||
payload["advancing_arms"] = [
|
||||
arm["name"]
|
||||
for arm in payload["arms"]
|
||||
if (arm.get("screen") or {}).get("advances")
|
||||
]
|
||||
payload["ranking_by_full_sharpe"] = [
|
||||
arm["name"]
|
||||
for arm in sorted(
|
||||
payload["arms"],
|
||||
key=lambda row: float(
|
||||
(row.get("full_book") or {}).get("sharpe") or -math.inf
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
]
|
||||
if args.keep_arm_reports:
|
||||
payload["arm_report_directory"] = str(work_dir)
|
||||
common._write_json(out_path, payload)
|
||||
_write_markdown(markdown_path, payload)
|
||||
|
||||
if not args.keep_arm_reports:
|
||||
for path in arm_outputs:
|
||||
path.unlink(missing_ok=True)
|
||||
work_dir.rmdir()
|
||||
|
||||
print("\nGTL confirmation matrix complete.")
|
||||
print(f" JSON: {out_path}")
|
||||
print(f" Markdown: {markdown_path}")
|
||||
if payload["advancing_arms"]:
|
||||
print(" Passing arms: " + ", ".join(payload["advancing_arms"]))
|
||||
else:
|
||||
print(" No arm passed every pre-registered screen.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,287 +0,0 @@
|
||||
"""Run the pre-registered GTL strength-confirmation sensitivity band.
|
||||
|
||||
The strength-1000 intersection was the only composition arm to improve
|
||||
full/train/test Sharpe, but it missed the unchanged drawdown guardrail. This
|
||||
matrix checks whether that result is a stable one-dimensional plateau rather
|
||||
than tuning the guardrail or launching another broad parameter search.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
from datetime import date, datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from scripts import run_gtl_confirmation_matrix as composition # noqa: E402
|
||||
from scripts import run_gtl_tuning_matrix as common # noqa: E402
|
||||
|
||||
REFERENCE_TUNING = ROOT / "reports" / "backtest-20260713-gtl-tuning-matrix.json"
|
||||
REFERENCE_COMPOSITION = (
|
||||
ROOT / "reports" / "backtest-20260713-gtl-confirmation-matrix.json"
|
||||
)
|
||||
STRENGTH_SCALES = (625.0, 750.0, 875.0, 1000.0, 1125.0, 1250.0, 1500.0, 2000.0)
|
||||
|
||||
GTL_STRENGTH_ARMS: tuple[dict[str, Any], ...] = (
|
||||
composition._arm(
|
||||
"control",
|
||||
"Frozen GTL composition-path parity control.",
|
||||
"intersection",
|
||||
),
|
||||
*(
|
||||
composition._arm(
|
||||
f"strength_{int(scale)}_intersection",
|
||||
f"Require control confirmation at traffic-strength scale {scale:g}.",
|
||||
"intersection",
|
||||
{"name": f"strength_{int(scale)}", "strength_scale": scale},
|
||||
)
|
||||
for scale in STRENGTH_SCALES
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"snapshot",
|
||||
nargs="?",
|
||||
default="backtest_snapshots/prod.sqlite",
|
||||
help="Local SQLite snapshot path.",
|
||||
)
|
||||
parser.add_argument("--workers", type=int, default=7)
|
||||
parser.add_argument("--holdout-split", default="2024-07-01")
|
||||
parser.add_argument(
|
||||
"--out",
|
||||
default=None,
|
||||
help=(
|
||||
"Consolidated JSON path. Defaults to "
|
||||
"reports/backtest-YYYYMMDD-gtl-strength-sensitivity.json."
|
||||
),
|
||||
)
|
||||
parser.add_argument("--keep-arm-reports", action="store_true")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _reference_arm(path: Path, name: str) -> dict | None:
|
||||
if not path.exists():
|
||||
return None
|
||||
with path.open(encoding="utf-8") as handle:
|
||||
payload = json.load(handle)
|
||||
return next(
|
||||
(arm for arm in payload.get("arms") or [] if arm.get("name") == name),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def _write_markdown(path: Path, payload: dict) -> None:
|
||||
rows = [
|
||||
"# GTL strength-confirmation sensitivity",
|
||||
"",
|
||||
f"Status: **{payload['status']}** ",
|
||||
f"Holdout split: `{payload['holdout_split']}` ",
|
||||
f"Completed arms: {len(payload['arms'])}/{payload['arm_count']}",
|
||||
"",
|
||||
"| Arm | Qualified | Full Sharpe | CAGR | Max DD | Trades | Train Sharpe | Test Sharpe | Ex-top-5% R | Screen |",
|
||||
"|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|",
|
||||
]
|
||||
for arm in payload["arms"]:
|
||||
full = arm.get("full_book") or {}
|
||||
holdout = arm.get("holdout") or {}
|
||||
screen = arm.get("screen") or {}
|
||||
rows.append(
|
||||
"| "
|
||||
+ " | ".join((
|
||||
arm["name"],
|
||||
str(arm.get("qualified") or "-"),
|
||||
common._fmt(full.get("sharpe")),
|
||||
common._fmt(full.get("cagr_pct"), 1),
|
||||
common._fmt(full.get("max_drawdown_pct"), 1),
|
||||
str(full.get("trades") or "-"),
|
||||
common._fmt((holdout.get("train") or {}).get("sharpe")),
|
||||
common._fmt((holdout.get("test") or {}).get("sharpe")),
|
||||
common._fmt(arm.get("qualified_net_avg_r_ex_top5"), 3),
|
||||
f"{screen.get('passed', '-')}/{screen.get('total', '-')}",
|
||||
))
|
||||
+ " |"
|
||||
)
|
||||
rows.extend((
|
||||
"",
|
||||
"## Pre-registered interpretation",
|
||||
"",
|
||||
"The six original guardrails remain unchanged. A stable candidate requires at least two adjacent non-control scales to pass all six; an isolated passing scale is rejected as sensitivity, not promoted.",
|
||||
"",
|
||||
))
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("\n".join(rows), encoding="utf-8")
|
||||
|
||||
|
||||
def _stable_plateau_pairs(arms: list[dict]) -> list[list[str]]:
|
||||
sensitivity = [arm for arm in arms if arm.get("name") != "control"]
|
||||
return [
|
||||
[left["name"], right["name"]]
|
||||
for left, right in zip(sensitivity, sensitivity[1:], strict=False)
|
||||
if (left.get("screen") or {}).get("advances")
|
||||
and (right.get("screen") or {}).get("advances")
|
||||
]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = _args()
|
||||
snapshot = Path(args.snapshot).resolve()
|
||||
if not snapshot.exists():
|
||||
raise SystemExit(f"Snapshot not found: {snapshot}")
|
||||
if args.workers < 1:
|
||||
raise SystemExit("--workers must be at least 1")
|
||||
try:
|
||||
date.fromisoformat(args.holdout_split)
|
||||
except ValueError as exc:
|
||||
raise SystemExit("--holdout-split must use YYYY-MM-DD") from exc
|
||||
|
||||
reference_control = _reference_arm(REFERENCE_TUNING, "control")
|
||||
reference_1000 = _reference_arm(
|
||||
REFERENCE_COMPOSITION,
|
||||
"strength_intersection",
|
||||
)
|
||||
if reference_control is None or reference_1000 is None:
|
||||
raise SystemExit(
|
||||
"The completed GTL tuning and confirmation matrices are required "
|
||||
"for control/replication checks"
|
||||
)
|
||||
|
||||
stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
|
||||
default_out = (
|
||||
ROOT / "reports" / f"backtest-{stamp[:8]}-gtl-strength-sensitivity.json"
|
||||
)
|
||||
out_path = Path(args.out) if args.out else default_out
|
||||
if not out_path.is_absolute():
|
||||
out_path = ROOT / out_path
|
||||
markdown_path = out_path.with_suffix(".md")
|
||||
work_dir = ROOT / "reports" / f".gtl-strength-work-{stamp}"
|
||||
work_dir.mkdir(parents=True, exist_ok=False)
|
||||
|
||||
payload: dict[str, Any] = {
|
||||
"status": "running",
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"snapshot": str(snapshot),
|
||||
"workers": args.workers,
|
||||
"holdout_split": args.holdout_split,
|
||||
"arm_count": len(GTL_STRENGTH_ARMS),
|
||||
"strength_scales": list(STRENGTH_SCALES),
|
||||
"arms": [],
|
||||
"promotion_rule": (
|
||||
"At least two adjacent non-control scales must pass all six original "
|
||||
"guardrails."
|
||||
),
|
||||
}
|
||||
common._write_json(out_path, payload)
|
||||
_write_markdown(markdown_path, payload)
|
||||
|
||||
control_report: dict | None = None
|
||||
arm_outputs: list[Path] = []
|
||||
try:
|
||||
for index, arm in enumerate(GTL_STRENGTH_ARMS, start=1):
|
||||
name = arm["name"]
|
||||
output = work_dir / f"{index:02d}-{name}.json"
|
||||
arm_outputs.append(output)
|
||||
print(f"\n[{index}/{len(GTL_STRENGTH_ARMS)}] {name}", flush=True)
|
||||
print(f" {arm['description']}", flush=True)
|
||||
composition._run_arm(
|
||||
arm,
|
||||
snapshot,
|
||||
args.workers,
|
||||
args.holdout_split,
|
||||
output,
|
||||
)
|
||||
report = common._load_report(output)
|
||||
compact = common._compact_arm(
|
||||
report,
|
||||
composition._config(arm),
|
||||
control_report,
|
||||
)
|
||||
compact["description"] = arm["description"]
|
||||
if control_report is None:
|
||||
control_report = report
|
||||
compact["screen"] = {
|
||||
"checks": {}, "passed": 0, "total": 0, "advances": False,
|
||||
}
|
||||
if composition._signature(compact) != composition._signature(
|
||||
reference_control
|
||||
):
|
||||
raise RuntimeError(
|
||||
"Strength sensitivity control does not reproduce frozen GTL control"
|
||||
)
|
||||
payload["control_parity"] = "pass"
|
||||
else:
|
||||
compact["screen"] = common._screen_arm(
|
||||
compact,
|
||||
payload["arms"][0],
|
||||
)
|
||||
if name == "strength_1000_intersection":
|
||||
if composition._signature(compact) != composition._signature(
|
||||
reference_1000
|
||||
):
|
||||
raise RuntimeError(
|
||||
"Strength-1000 arm does not reproduce the completed composition result"
|
||||
)
|
||||
payload["strength_1000_replication"] = "pass"
|
||||
payload["arms"].append(compact)
|
||||
payload["completed_at"] = datetime.now(timezone.utc).isoformat()
|
||||
common._write_json(out_path, payload)
|
||||
_write_markdown(markdown_path, payload)
|
||||
except BaseException as exc:
|
||||
payload["status"] = "failed"
|
||||
payload["failed_at"] = datetime.now(timezone.utc).isoformat()
|
||||
payload["error"] = f"{type(exc).__name__}: {exc}"
|
||||
payload["work_dir"] = str(work_dir)
|
||||
common._write_json(out_path, payload)
|
||||
_write_markdown(markdown_path, payload)
|
||||
raise
|
||||
|
||||
payload["status"] = "complete"
|
||||
payload["completed_at"] = datetime.now(timezone.utc).isoformat()
|
||||
payload["advancing_arms"] = [
|
||||
arm["name"]
|
||||
for arm in payload["arms"]
|
||||
if (arm.get("screen") or {}).get("advances")
|
||||
]
|
||||
payload["stable_plateau_pairs"] = _stable_plateau_pairs(payload["arms"])
|
||||
payload["stable_candidate"] = bool(payload["stable_plateau_pairs"])
|
||||
payload["ranking_by_full_sharpe"] = [
|
||||
arm["name"]
|
||||
for arm in sorted(
|
||||
payload["arms"],
|
||||
key=lambda row: float(
|
||||
(row.get("full_book") or {}).get("sharpe") or -math.inf
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
]
|
||||
if args.keep_arm_reports:
|
||||
payload["arm_report_directory"] = str(work_dir)
|
||||
common._write_json(out_path, payload)
|
||||
_write_markdown(markdown_path, payload)
|
||||
|
||||
if not args.keep_arm_reports:
|
||||
for path in arm_outputs:
|
||||
path.unlink(missing_ok=True)
|
||||
work_dir.rmdir()
|
||||
|
||||
print("\nGTL strength sensitivity complete.")
|
||||
print(f" JSON: {out_path}")
|
||||
print(f" Markdown: {markdown_path}")
|
||||
if payload["stable_candidate"]:
|
||||
pairs = [" + ".join(pair) for pair in payload["stable_plateau_pairs"]]
|
||||
print(" Stable passing plateau: " + "; ".join(pairs))
|
||||
else:
|
||||
print(" No stable adjacent passing plateau.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,418 +0,0 @@
|
||||
"""Run the complete Gate Target Ladder tuning matrix with one command.
|
||||
|
||||
Every arm is a full production-parity backtest. Arms run sequentially so each
|
||||
one can use the requested worker pool without competing with another arm. Large
|
||||
per-arm reports live in a temporary run directory and are removed after a
|
||||
successful consolidation unless ``--keep-arm-reports`` is supplied.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import date, datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
RUNNER = ROOT / "scripts" / "run_backtest_snapshot.py"
|
||||
|
||||
BASE_CONFIG: dict[str, Any] = {
|
||||
"lookback_bars": None,
|
||||
"grid_bins": 20,
|
||||
"include_pivots": True,
|
||||
"pivot_window": 2,
|
||||
"touch_tolerance": 0.005,
|
||||
"merge_tolerance": 0.005,
|
||||
"strength_scale": 500.0,
|
||||
"zone_tolerance": 0.02,
|
||||
"candidate_limit": 5,
|
||||
"max_target_atr": None,
|
||||
}
|
||||
|
||||
# Single-variable arms only. The control value is represented by BASE_CONFIG;
|
||||
# there is deliberately no Cartesian product.
|
||||
GTL_TUNING_ARMS: tuple[dict[str, Any], ...] = (
|
||||
{"name": "control", "description": "Frozen explicit GTL defaults."},
|
||||
{"name": "lookback_252", "description": "One-year GTL history.", "lookback_bars": 252},
|
||||
{"name": "lookback_504", "description": "Two-year GTL history.", "lookback_bars": 504},
|
||||
{"name": "lookback_756", "description": "Three-year GTL history.", "lookback_bars": 756},
|
||||
{"name": "candidates_8", "description": "Retain up to eight candidates before probability.", "candidate_limit": 8},
|
||||
{"name": "candidates_all", "description": "Score every eligible target before primary selection.", "candidate_limit": None},
|
||||
{"name": "max_atr_5_5", "description": "Universal 5.5 ATR maximum target distance.", "max_target_atr": 5.5},
|
||||
{"name": "max_atr_8", "description": "Universal 8 ATR maximum target distance.", "max_target_atr": 8.0},
|
||||
{"name": "touch_0", "description": "Strict candle-range crossings with no touch padding.", "touch_tolerance": 0.0},
|
||||
{"name": "touch_0_25pct", "description": "Use 0.25% padding when counting price traffic.", "touch_tolerance": 0.0025},
|
||||
{"name": "merge_0_25pct", "description": "Merge GTL proposals within 0.25%.", "merge_tolerance": 0.0025},
|
||||
{"name": "merge_1pct", "description": "Merge GTL proposals within 1%.", "merge_tolerance": 0.01},
|
||||
{"name": "zones_1pct", "description": "Cluster target zones within 1%.", "zone_tolerance": 0.01},
|
||||
{"name": "zones_3pct", "description": "Cluster target zones within 3%.", "zone_tolerance": 0.03},
|
||||
{"name": "grid_12", "description": "Use 12 evenly spaced range centers.", "grid_bins": 12},
|
||||
{"name": "grid_32", "description": "Use 32 evenly spaced range centers.", "grid_bins": 32},
|
||||
{"name": "pivots_none", "description": "Range grid only; omit swing pivots.", "include_pivots": False},
|
||||
{"name": "pivots_11bar", "description": "Use an 11-bar swing-pivot window.", "pivot_window": 5},
|
||||
{"name": "strength_250", "description": "Slower traffic-strength saturation.", "strength_scale": 250.0},
|
||||
{"name": "strength_1000", "description": "Faster traffic-strength saturation.", "strength_scale": 1000.0},
|
||||
)
|
||||
|
||||
BOOK_FIELDS = (
|
||||
"sharpe",
|
||||
"cagr_pct",
|
||||
"max_drawdown_pct",
|
||||
"trades",
|
||||
"win_rate",
|
||||
"avg_hold_days",
|
||||
"skipped_book_full",
|
||||
)
|
||||
|
||||
|
||||
def _args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"snapshot",
|
||||
nargs="?",
|
||||
default="backtest_snapshots/prod.sqlite",
|
||||
help="Local SQLite snapshot path.",
|
||||
)
|
||||
parser.add_argument("--workers", type=int, default=7)
|
||||
parser.add_argument(
|
||||
"--holdout-split",
|
||||
default="2024-07-01",
|
||||
help="Disjoint train/test split included in every arm.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--out",
|
||||
default=None,
|
||||
help="Consolidated JSON path. Defaults to reports/backtest-YYYYMMDD-gtl-tuning-matrix.json.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--keep-arm-reports",
|
||||
action="store_true",
|
||||
help="Keep the large temporary per-arm JSON reports after consolidation.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _arm_config(arm: dict[str, Any]) -> dict[str, Any]:
|
||||
config = {**BASE_CONFIG, **{key: value for key, value in arm.items() if key != "description"}}
|
||||
return config
|
||||
|
||||
|
||||
def _load_report(path: Path) -> dict:
|
||||
with path.open(encoding="utf-8") as handle:
|
||||
report = json.load(handle)
|
||||
if report.get("sr_candidate_audit") is None:
|
||||
raise ValueError(f"Report lacks sr_candidate_audit: {path}")
|
||||
return report
|
||||
|
||||
|
||||
def _compact_book(row: dict | None) -> dict | None:
|
||||
if row is None:
|
||||
return None
|
||||
return {key: row.get(key) for key in BOOK_FIELDS}
|
||||
|
||||
|
||||
def _full_book(report: dict) -> dict | None:
|
||||
runs = ((report.get("portfolio_monitor") or {}).get("runs") or [])
|
||||
return _compact_book(next(
|
||||
(
|
||||
row
|
||||
for row in runs
|
||||
if row.get("is_production") and row.get("lookback") == "all"
|
||||
),
|
||||
None,
|
||||
))
|
||||
|
||||
|
||||
def _holdout_books(report: dict) -> dict[str, dict | None]:
|
||||
rows = ((report.get("holdout") or {}).get("rows") or [])
|
||||
return {
|
||||
window: _compact_book(next((row for row in rows if row.get("window") == window), None))
|
||||
for window in ("train", "test")
|
||||
}
|
||||
|
||||
|
||||
def _audit_key(row: dict) -> tuple[str, str, str]:
|
||||
return row["symbol"], row["date"], row["direction"]
|
||||
|
||||
|
||||
def _cohort_stats(rows: list[dict]) -> dict:
|
||||
net = [float(row.get("net_r", 0.0)) for row in rows]
|
||||
trimmed = sorted(net, reverse=True)[math.ceil(len(net) * 0.05):]
|
||||
return {
|
||||
"count": len(rows),
|
||||
"net_avg_r": round(sum(net) / len(net), 4) if net else None,
|
||||
"net_avg_r_ex_top5": round(sum(trimmed) / len(trimmed), 4) if trimmed else None,
|
||||
}
|
||||
|
||||
|
||||
def _cohort_comparison(control: dict, variant: dict) -> dict:
|
||||
control_rows = {
|
||||
_audit_key(row): row for row in control.get("sr_candidate_audit") or []
|
||||
}
|
||||
variant_rows = {
|
||||
_audit_key(row): row for row in variant.get("sr_candidate_audit") or []
|
||||
}
|
||||
control_q = {key for key, row in control_rows.items() if row.get("qualified")}
|
||||
variant_q = {key for key, row in variant_rows.items() if row.get("qualified")}
|
||||
retained = control_q & variant_q
|
||||
added = variant_q - control_q
|
||||
removed = control_q - variant_q
|
||||
return {
|
||||
"retained": _cohort_stats([variant_rows[key] for key in retained]),
|
||||
"added": _cohort_stats([variant_rows[key] for key in added]),
|
||||
"removed": _cohort_stats([control_rows[key] for key in removed]),
|
||||
}
|
||||
|
||||
|
||||
def _compact_arm(report: dict, config: dict, control: dict | None) -> dict:
|
||||
qualified = report.get("overall_qualified") or {}
|
||||
result = {
|
||||
"name": config["name"],
|
||||
"config": config,
|
||||
"candidates": report.get("candidates"),
|
||||
"qualified": report.get("qualified"),
|
||||
"qualified_net_avg_r": qualified.get("net_avg_r"),
|
||||
"qualified_net_avg_r_ex_top5": qualified.get("net_avg_r_ex_top5"),
|
||||
"full_book": _full_book(report),
|
||||
"holdout": _holdout_books(report),
|
||||
"gtl_diagnostics": report.get("sr_variant_diagnostics"),
|
||||
"cohort_vs_control": (
|
||||
_cohort_comparison(control, report) if control is not None else None
|
||||
),
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def _screen_arm(arm: dict, control: dict) -> dict:
|
||||
full = arm.get("full_book") or {}
|
||||
base_full = control.get("full_book") or {}
|
||||
train = (arm.get("holdout") or {}).get("train") or {}
|
||||
base_train = (control.get("holdout") or {}).get("train") or {}
|
||||
test = (arm.get("holdout") or {}).get("test") or {}
|
||||
base_test = (control.get("holdout") or {}).get("test") or {}
|
||||
|
||||
def at_least(value: Any, baseline: Any) -> bool:
|
||||
return value is not None and baseline is not None and float(value) >= float(baseline)
|
||||
|
||||
control_trades = float(base_full.get("trades") or 0.0)
|
||||
arm_trades = float(full.get("trades") or 0.0)
|
||||
checks = {
|
||||
"full_sharpe_not_worse": at_least(full.get("sharpe"), base_full.get("sharpe")),
|
||||
"train_sharpe_not_worse": at_least(train.get("sharpe"), base_train.get("sharpe")),
|
||||
"test_sharpe_not_worse": at_least(test.get("sharpe"), base_test.get("sharpe")),
|
||||
"drawdown_not_worse": (
|
||||
full.get("max_drawdown_pct") is not None
|
||||
and base_full.get("max_drawdown_pct") is not None
|
||||
and abs(float(full["max_drawdown_pct"]))
|
||||
<= abs(float(base_full["max_drawdown_pct"]))
|
||||
),
|
||||
"retains_80pct_trades": control_trades > 0 and arm_trades >= control_trades * 0.8,
|
||||
"robust_expectancy_positive": (
|
||||
arm.get("qualified_net_avg_r_ex_top5") is not None
|
||||
and float(arm["qualified_net_avg_r_ex_top5"]) > 0
|
||||
),
|
||||
}
|
||||
return {
|
||||
"checks": checks,
|
||||
"passed": sum(checks.values()),
|
||||
"total": len(checks),
|
||||
"advances": all(checks.values()),
|
||||
}
|
||||
|
||||
|
||||
def _write_json(path: Path, payload: dict) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w", encoding="utf-8") as handle:
|
||||
json.dump(payload, handle, indent=2)
|
||||
handle.write("\n")
|
||||
|
||||
|
||||
def _fmt(value: Any, digits: int = 2) -> str:
|
||||
return "-" if value is None else f"{float(value):.{digits}f}"
|
||||
|
||||
|
||||
def _write_markdown(path: Path, payload: dict) -> None:
|
||||
rows = [
|
||||
"# GTL tuning matrix",
|
||||
"",
|
||||
f"Status: **{payload['status']}** ",
|
||||
f"Holdout split: `{payload['holdout_split']}` ",
|
||||
f"Completed arms: {len(payload['arms'])}/{payload['arm_count']}",
|
||||
"",
|
||||
"| Arm | Full Sharpe | CAGR | Max DD | Trades | Train Sharpe | Test Sharpe | Ex-top-5% R | Screen |",
|
||||
"|---|---:|---:|---:|---:|---:|---:|---:|---:|",
|
||||
]
|
||||
for arm in payload["arms"]:
|
||||
full = arm.get("full_book") or {}
|
||||
holdout = arm.get("holdout") or {}
|
||||
train = holdout.get("train") or {}
|
||||
test = holdout.get("test") or {}
|
||||
screen = arm.get("screen") or {}
|
||||
rows.append(
|
||||
"| "
|
||||
+ " | ".join((
|
||||
arm["name"],
|
||||
_fmt(full.get("sharpe")),
|
||||
_fmt(full.get("cagr_pct"), 1),
|
||||
_fmt(full.get("max_drawdown_pct"), 1),
|
||||
str(full.get("trades") or "-"),
|
||||
_fmt(train.get("sharpe")),
|
||||
_fmt(test.get("sharpe")),
|
||||
_fmt(arm.get("qualified_net_avg_r_ex_top5"), 3),
|
||||
f"{screen.get('passed', '-')}/{screen.get('total', '-')}",
|
||||
))
|
||||
+ " |"
|
||||
)
|
||||
rows.extend((
|
||||
"",
|
||||
"## Interpretation guardrail",
|
||||
"",
|
||||
"The post-2024 interval has already informed prior research. The train/test columns are robustness checks, not a pristine holdout. A passing arm is a candidate for forward paper validation, not automatic production promotion.",
|
||||
"",
|
||||
))
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("\n".join(rows), encoding="utf-8")
|
||||
|
||||
|
||||
def _run_arm(
|
||||
*,
|
||||
arm: dict[str, Any],
|
||||
snapshot: str,
|
||||
workers: int,
|
||||
holdout_split: str,
|
||||
output: Path,
|
||||
) -> None:
|
||||
config = _arm_config(arm)
|
||||
command = [
|
||||
sys.executable,
|
||||
str(RUNNER),
|
||||
snapshot,
|
||||
"--workers", str(workers),
|
||||
"--allow-spawn",
|
||||
"--sr-variant", "gtl_tuning",
|
||||
"--gtl-config", json.dumps(config, separators=(",", ":")),
|
||||
"--holdout-split", holdout_split,
|
||||
"--sr-audit",
|
||||
"--out", str(output),
|
||||
]
|
||||
subprocess.run(command, cwd=ROOT, check=True)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = _args()
|
||||
snapshot = Path(args.snapshot).resolve()
|
||||
if not snapshot.exists():
|
||||
raise SystemExit(f"Snapshot not found: {snapshot}")
|
||||
if args.workers < 1:
|
||||
raise SystemExit("--workers must be at least 1")
|
||||
try:
|
||||
date.fromisoformat(args.holdout_split)
|
||||
except ValueError as exc:
|
||||
raise SystemExit("--holdout-split must use YYYY-MM-DD") from exc
|
||||
|
||||
stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
|
||||
default_out = ROOT / "reports" / f"backtest-{stamp[:8]}-gtl-tuning-matrix.json"
|
||||
out_path = Path(args.out) if args.out else default_out
|
||||
if not out_path.is_absolute():
|
||||
out_path = ROOT / out_path
|
||||
markdown_path = out_path.with_suffix(".md")
|
||||
work_dir = ROOT / "reports" / f".gtl-tuning-work-{stamp}"
|
||||
work_dir.mkdir(parents=True, exist_ok=False)
|
||||
|
||||
payload: dict[str, Any] = {
|
||||
"status": "running",
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"snapshot": str(snapshot.resolve()),
|
||||
"workers": args.workers,
|
||||
"holdout_split": args.holdout_split,
|
||||
"arm_count": len(GTL_TUNING_ARMS),
|
||||
"arms": [],
|
||||
"caveat": (
|
||||
"The split is a robustness check, not a pristine holdout; post-2024 "
|
||||
"data has already informed earlier research."
|
||||
),
|
||||
}
|
||||
_write_json(out_path, payload)
|
||||
_write_markdown(markdown_path, payload)
|
||||
|
||||
control_report: dict | None = None
|
||||
arm_outputs: list[Path] = []
|
||||
try:
|
||||
for index, arm in enumerate(GTL_TUNING_ARMS, start=1):
|
||||
name = str(arm["name"])
|
||||
output = work_dir / f"{index:02d}-{name}.json"
|
||||
arm_outputs.append(output)
|
||||
print(f"\n[{index}/{len(GTL_TUNING_ARMS)}] GTL arm: {name}", flush=True)
|
||||
print(f" {arm['description']}", flush=True)
|
||||
_run_arm(
|
||||
arm=arm,
|
||||
snapshot=str(snapshot),
|
||||
workers=args.workers,
|
||||
holdout_split=args.holdout_split,
|
||||
output=output,
|
||||
)
|
||||
report = _load_report(output)
|
||||
config = _arm_config(arm)
|
||||
compact = _compact_arm(report, config, control_report)
|
||||
compact["description"] = arm["description"]
|
||||
if control_report is None:
|
||||
control_report = report
|
||||
compact["screen"] = {
|
||||
"checks": {},
|
||||
"passed": 0,
|
||||
"total": 0,
|
||||
"advances": False,
|
||||
}
|
||||
else:
|
||||
compact["screen"] = _screen_arm(compact, payload["arms"][0])
|
||||
payload["arms"].append(compact)
|
||||
payload["completed_at"] = datetime.now(timezone.utc).isoformat()
|
||||
_write_json(out_path, payload)
|
||||
_write_markdown(markdown_path, payload)
|
||||
except Exception as exc:
|
||||
payload["status"] = "failed"
|
||||
payload["failed_at"] = datetime.now(timezone.utc).isoformat()
|
||||
payload["error"] = f"{type(exc).__name__}: {exc}"
|
||||
payload["work_dir"] = str(work_dir)
|
||||
_write_json(out_path, payload)
|
||||
_write_markdown(markdown_path, payload)
|
||||
raise
|
||||
|
||||
payload["status"] = "complete"
|
||||
payload["completed_at"] = datetime.now(timezone.utc).isoformat()
|
||||
payload["advancing_arms"] = [
|
||||
arm["name"] for arm in payload["arms"] if (arm.get("screen") or {}).get("advances")
|
||||
]
|
||||
payload["ranking_by_full_sharpe"] = [
|
||||
arm["name"]
|
||||
for arm in sorted(
|
||||
payload["arms"],
|
||||
key=lambda row: float((row.get("full_book") or {}).get("sharpe") or -math.inf),
|
||||
reverse=True,
|
||||
)
|
||||
]
|
||||
if args.keep_arm_reports:
|
||||
payload["arm_report_directory"] = str(work_dir)
|
||||
_write_json(out_path, payload)
|
||||
_write_markdown(markdown_path, payload)
|
||||
|
||||
if not args.keep_arm_reports:
|
||||
for path in arm_outputs:
|
||||
path.unlink(missing_ok=True)
|
||||
work_dir.rmdir()
|
||||
|
||||
print("\nGTL tuning matrix complete.")
|
||||
print(f" JSON: {out_path}")
|
||||
print(f" Markdown: {markdown_path}")
|
||||
if payload["advancing_arms"]:
|
||||
print(f" Arms passing every pre-registered screen: {', '.join(payload['advancing_arms'])}")
|
||||
else:
|
||||
print(" No arm passed every pre-registered screen.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user