aether-quant 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: aether-quant
3
+ Version: 0.2.0
4
+ Summary: Aether Quant convenience CLI
5
+ Requires-Python: >=3.10
@@ -0,0 +1,12 @@
1
+ aq_cli.py,sha256=0v_TC-nHdm7pPBMNOBOGGCkxQB2In6H6U9mDhow8isA,13467
2
+ risk/README.md,sha256=l9EBeIVirsIW4zw9rMZY9dLRfci_MZbVM2PYRWyGf-k,608
3
+ risk/__init__.py,sha256=DpEPHZV9GRLE3Wi731QMZ6J0e0moJsfCe6KH0OfRKK0,492
4
+ risk/manual_override.py,sha256=XwQYAfGlKtyXiJfAF_P9FNsOfLjfYG4HRHVySLFEhAk,2595
5
+ risk/position_sizing.py,sha256=UvAvehp5hPQX3-qm-F2dDKzQ5hpgUiRnH-asrKt3UAI,3981
6
+ aether_quant-0.2.0.dist-info/METADATA,sha256=F4r2u5uBzS9GlSh5Yf1uTwPEAF95FDlzYQutz7AHikc,118
7
+ aether_quant-0.2.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
8
+ aether_quant-0.2.0.dist-info/entry_points.txt,sha256=QFFJIvHqEKjBKlVrniDI73HObpCTR3rECR22vsJtvxQ,35
9
+ aether_quant-0.2.0.dist-info/scm_file_list.json,sha256=gI7-wezKOZOTbrJzS4RFGIa87mmdX1FaahVN2WHkQQQ,7642
10
+ aether_quant-0.2.0.dist-info/scm_version.json,sha256=WaH5Lrm-sEC3pB9zIMQHDkWtTYjcdL5L3HxqppJdaKg,160
11
+ aether_quant-0.2.0.dist-info/top_level.txt,sha256=SAJI6n0gUv2UeUVOJ3PEEJEHiOoFrbQNLIrnBpcdMA0,12
12
+ aether_quant-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ aq = aq_cli:main
@@ -0,0 +1,211 @@
1
+ {
2
+ "files": [
3
+ "README.md",
4
+ "aq_cli.py",
5
+ "Dockerfile",
6
+ "pyproject.toml",
7
+ ".env.compose.example",
8
+ "Dockerfile.retraining_worker",
9
+ "train_topology.py",
10
+ "train.py",
11
+ ".dockerignore",
12
+ "Dockerfile.telegram_worker",
13
+ "Dockerfile.worker",
14
+ ".gitattributes",
15
+ "main.py",
16
+ ".gitignore",
17
+ "docker-compose.yml",
18
+ "risk_controls.py",
19
+ "Dockerfile.trigger_worker",
20
+ "experience/redis_queue.py",
21
+ "experience/README.md",
22
+ "experience/__init__.py",
23
+ "experience/simulated_portfolio.py",
24
+ "experience/observation_metrics.py",
25
+ "experience/postgres_worker.py",
26
+ "risk/README.md",
27
+ "risk/__init__.py",
28
+ "risk/manual_override.py",
29
+ "risk/position_sizing.py",
30
+ "notifications/telegram_client.py",
31
+ "notifications/README.md",
32
+ "notifications/__init__.py",
33
+ "notifications/telegram_worker.py",
34
+ "notifications/postgres_telegram.py",
35
+ "notifications/telegram_alerts.py",
36
+ "topology/README.md",
37
+ "topology/__init__.py",
38
+ "topology/market_topology.py",
39
+ "topology/learned_topology.py",
40
+ "analyzer/README.md",
41
+ "analyzer/market_analyzer.py",
42
+ "analyzer/__init__.py",
43
+ "data_pipeline/README.md",
44
+ "data_pipeline/__init__.py",
45
+ "data_pipeline/v2_manifest.py",
46
+ "data_pipeline/yfinance_backfill.py",
47
+ "visualization/README.md",
48
+ "regime/README.md",
49
+ "regime/__init__.py",
50
+ "regime/market_regime.py",
51
+ "moe/README.md",
52
+ "moe/__init__.py",
53
+ "moe/gating.py",
54
+ "backtests/.gitkeep",
55
+ "retraining/README.md",
56
+ "retraining/lean_backtest.py",
57
+ "retraining/__init__.py",
58
+ "retraining/artifacts.py",
59
+ "retraining/planning.py",
60
+ "retraining/vault_commands.py",
61
+ "retraining/backtest_gate.py",
62
+ "retraining/validation_gate.py",
63
+ "retraining/vault_client.py",
64
+ "retraining/worker.py",
65
+ "retraining/status_export.py",
66
+ "retraining/postgres_registry.py",
67
+ "retraining/orchestrator.py",
68
+ "execution/README.md",
69
+ "execution/__init__.py",
70
+ "execution/order_gate.py",
71
+ "monitoring/neural_network_state.py",
72
+ "monitoring/README.md",
73
+ "monitoring/__init__.py",
74
+ "monitoring/api_server.py",
75
+ "performance/README.md",
76
+ "performance/__init__.py",
77
+ "performance/triggers.py",
78
+ "performance/postgres_triggers.py",
79
+ "performance/trigger_worker.py",
80
+ "liquidity/README.md",
81
+ "liquidity/__init__.py",
82
+ "liquidity/market_liquidity.py",
83
+ "experts/expert_datasets.py",
84
+ "experts/README.md",
85
+ "experts/__init__.py",
86
+ "webui/package-lock.json",
87
+ "webui/README.md",
88
+ "webui/risk_v2_12.png",
89
+ "webui/tsconfig.json",
90
+ "webui/.oxlintrc.json",
91
+ "webui/tsconfig.node.json",
92
+ "webui/tsconfig.app.json",
93
+ "webui/index.html",
94
+ "webui/.gitignore",
95
+ "webui/vite.config.ts",
96
+ "webui/overview_v2_12.png",
97
+ "webui/package.json",
98
+ "webui/src/App.tsx",
99
+ "webui/src/index.css",
100
+ "webui/src/main.tsx",
101
+ "webui/src/api/hooks.ts",
102
+ "webui/src/api/client.ts",
103
+ "webui/src/lib/downsample.ts",
104
+ "webui/src/lib/format.ts",
105
+ "webui/src/components/scene3d/Scene3D.tsx",
106
+ "webui/src/components/risk/RiskCore.tsx",
107
+ "webui/src/components/risk/AssetSizingTable.tsx",
108
+ "webui/src/components/risk/LiquidityTable.tsx",
109
+ "webui/src/components/risk/RiskBar.tsx",
110
+ "webui/src/components/risk/StrategyRiskCards.tsx",
111
+ "webui/src/components/heatmap/AssetHeatmap.tsx",
112
+ "webui/src/components/topology/TopologyScene3D.tsx",
113
+ "webui/src/components/topology/ClusterList.tsx",
114
+ "webui/src/components/topology/TopologyLearningPanel.tsx",
115
+ "webui/src/components/layout/Panel.tsx",
116
+ "webui/src/components/layout/AppShell.tsx",
117
+ "webui/src/components/scorecards/Scorecards.tsx",
118
+ "webui/src/components/signals/PositionsList.tsx",
119
+ "webui/src/components/signals/Badge.tsx",
120
+ "webui/src/components/signals/SignalBoard.tsx",
121
+ "webui/src/components/neuralnet/NeuralNetworkScene3D.tsx",
122
+ "webui/src/components/neuralnet/NeuralNetworkStatsPanel.tsx",
123
+ "webui/src/components/monitoring/RetrainingStatusPanel.tsx",
124
+ "webui/src/components/monitoring/ObservationPanel.tsx",
125
+ "webui/src/components/monitoring/PerformanceTriggersPanel.tsx",
126
+ "webui/src/components/monitoring/CountTable.tsx",
127
+ "webui/src/components/monitoring/RawStateViewer.tsx",
128
+ "webui/src/components/monitoring/MonitoringFeeds.tsx",
129
+ "webui/src/components/tracing/BacktestEquityPanel.tsx",
130
+ "webui/src/components/tracing/ObservationEquityPanel.tsx",
131
+ "webui/src/components/tracing/MetricsSnapshotPanel.tsx",
132
+ "webui/src/components/tracing/DivergingBarChart.tsx",
133
+ "webui/src/components/tracing/LineChart.tsx",
134
+ "webui/src/components/tracing/AssetPerformancePanel.tsx",
135
+ "webui/src/pages/TopologyPage.tsx",
136
+ "webui/src/pages/Overview.tsx",
137
+ "webui/src/pages/RiskPage.tsx",
138
+ "webui/src/pages/TracingPage.tsx",
139
+ "webui/src/pages/NeuralNetworkPage.tsx",
140
+ "webui/src/types/tracing.ts",
141
+ "webui/src/types/state.ts",
142
+ "webui/public/favicon.svg",
143
+ "webui/public/icons.svg",
144
+ "development/README.md",
145
+ "development/Problems.md",
146
+ "development/Changelog.md",
147
+ "development/v2_architecture.md",
148
+ "development/infrastructure.md",
149
+ "tests/test_order_gate.py",
150
+ "tests/test_telegram_alerts.py",
151
+ "tests/test_trigger_worker.py",
152
+ "tests/test_v2_pipeline_manifest.py",
153
+ "tests/test_aq_cli.py",
154
+ "tests/test_risk_controls.py",
155
+ "tests/test_backtest_gate.py",
156
+ "tests/README.md",
157
+ "tests/test_status_export.py",
158
+ "tests/test_postgres_worker.py",
159
+ "tests/test_retraining_artifacts.py",
160
+ "tests/test_expert_datasets.py",
161
+ "tests/test_expert_models.py",
162
+ "tests/test_retraining_orchestrator.py",
163
+ "tests/test_telegram_client.py",
164
+ "tests/test_yfinance_backfill.py",
165
+ "tests/test_neural_network_state.py",
166
+ "tests/test_market_regime.py",
167
+ "tests/test_vault_client.py",
168
+ "tests/test_validation_gate.py",
169
+ "tests/test_observation_metrics.py",
170
+ "tests/test_train_topology.py",
171
+ "tests/test_telegram_worker.py",
172
+ "tests/test_triggers.py",
173
+ "tests/test_simulated_portfolio.py",
174
+ "tests/test_position_sizing.py",
175
+ "tests/test_market_liquidity.py",
176
+ "tests/test_lean_backtest_ml_coverage.py",
177
+ "tests/test_market_analyzer.py",
178
+ "tests/test_lean_backtest.py",
179
+ "tests/test_retraining_planning.py",
180
+ "tests/test_market_topology.py",
181
+ "tests/test_postgres_triggers.py",
182
+ "tests/test_manual_override.py",
183
+ "tests/test_postgres_telegram.py",
184
+ "tests/test_retraining_worker.py",
185
+ "tests/test_retraining_postgres_registry.py",
186
+ "tests/test_learned_topology.py",
187
+ "tests/test_gating_network.py",
188
+ "tests/test_train_pipeline.py",
189
+ "tests/test_experience_queue.py",
190
+ "tests/test_vault_commands.py",
191
+ "data/alternative/sec/aapl/.keep",
192
+ "data/alternative/trading-economics/.keep",
193
+ "data/alternative/trading-economics/calendar/.keep",
194
+ "data/alternative/trading-economics/indicator/.keep",
195
+ "data/alternative/trading-economics/earnings/.keep",
196
+ "data/alternative/estimize/estimate/.keep",
197
+ "data/alternative/estimize/release/.keep",
198
+ "data/alternative/estimize/consensus/aapl/.keep",
199
+ "requirements/requirements-runtime.txt",
200
+ "requirements/README.md",
201
+ "requirements/requirements-telegram-worker.txt",
202
+ "requirements/requirements.txt",
203
+ "requirements/requirements-worker.txt",
204
+ "requirements/requirements-trigger-worker.txt",
205
+ "requirements/requirements-dev.txt",
206
+ "requirements/requirements-retraining-worker.txt",
207
+ "ml/README.md",
208
+ ".github/workflows/release.yml",
209
+ ".github/workflows/ci.yml"
210
+ ]
211
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.2.0",
3
+ "distance": 0,
4
+ "node": "gaf8d80bac1f1b574de4f070bda52817a11973f49",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-04"
8
+ }
@@ -0,0 +1,2 @@
1
+ aq_cli
2
+ risk
aq_cli.py ADDED
@@ -0,0 +1,329 @@
1
+ """`aq` — a thin convenience CLI wrapping Aether Quant's day-to-day commands.
2
+
3
+ Matches this codebase's existing CLI convention exactly (see
4
+ `retraining/orchestrator.py`'s `argparse` + `subparsers.add_parser(...)`
5
+ shape) - a single-file dispatcher, not a framework. Every subcommand other
6
+ than `trade-lock` is a thin `subprocess.run(...)` wrapper around a command
7
+ that already exists and is already documented elsewhere (README.md,
8
+ development/infrastructure.md) - no logic is reimplemented here, this file
9
+ only saves typing. `trade-lock` is the one exception: it calls
10
+ `risk/manual_override.py` directly, no subprocess.
11
+
12
+ Deliberately scoped for v1 - wraps the commands already in daily use, not
13
+ every command mentioned anywhere in the project. Designed to be extended
14
+ incrementally: add a new `subparsers.add_parser(...)` block plus one `elif`
15
+ branch in `main()` for each new command, following the existing pattern.
16
+
17
+ Install once (registers the `aq` command on PATH inside the active venv):
18
+ pip install -e .
19
+ Then:
20
+ aq --help
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import argparse
26
+ import json
27
+ import os
28
+ import shutil
29
+ import subprocess
30
+ import sys
31
+ import time
32
+ import urllib.request
33
+ from importlib.metadata import version as installed_version
34
+ from pathlib import Path
35
+
36
+ from risk.manual_override import read_manual_trade_lock_override, write_manual_trade_lock_override
37
+
38
+ ROOT_DIR = Path(__file__).resolve().parent
39
+ CONFIG_PATH = ROOT_DIR / "config.json"
40
+ WEBUI_DIR = ROOT_DIR / "webui"
41
+
42
+ PACKAGE_NAME = "aether-quant"
43
+ UPDATE_CACHE_PATH = Path.home() / ".aq" / "update_check.json"
44
+ UPDATE_CHECK_INTERVAL_SECONDS = 24 * 60 * 60
45
+ UPDATE_CHECK_TIMEOUT_SECONDS = 2
46
+
47
+
48
+ def _run(cmd: list[str], cwd: Path = ROOT_DIR) -> int:
49
+ """Runs a command with inherited stdout/stderr (live output), returns its exit code."""
50
+ result = subprocess.run(cmd, cwd=str(cwd))
51
+ return result.returncode
52
+
53
+
54
+ def _find_quantconnect_lean_binary() -> str | None:
55
+ """Plain `lean` on PATH is ambiguous on machines with `elan` (Lean 4, the
56
+ theorem prover) installed - it ships its own `lean` binary under the same
57
+ name as QuantConnect's Lean CLI (`pip install lean`). Disambiguate by
58
+ checking `--version` output (Lean 4 prints "Lean (version 4...."; the
59
+ QuantConnect CLI does not), preferring this repo's own venv first."""
60
+ bin_dir_name = "Scripts" if sys.platform == "win32" else "bin"
61
+ binary_name = "lean.exe" if sys.platform == "win32" else "lean"
62
+ candidates = [str(ROOT_DIR / ".venv" / bin_dir_name / binary_name)]
63
+ on_path = shutil.which("lean")
64
+ if on_path:
65
+ candidates.append(on_path)
66
+
67
+ for candidate in candidates:
68
+ if candidate != on_path and not Path(candidate).exists():
69
+ continue
70
+ try:
71
+ result = subprocess.run([candidate, "--version"], capture_output=True, text=True, timeout=30)
72
+ except (FileNotFoundError, OSError, subprocess.TimeoutExpired):
73
+ continue
74
+ if "Lean (version" not in (result.stdout or "") + (result.stderr or ""):
75
+ return candidate
76
+ return None
77
+
78
+
79
+ def _parse_simple_version(value: str) -> tuple[int, ...] | None:
80
+ """Best-effort "X.Y.Z" -> (X, Y, Z) parse. Returns None for anything that
81
+ isn't a clean dotted-integer release version - dev/local builds (e.g.
82
+ setuptools-scm's "0.1.dev35+gc744f9ca4.d20260704" fallback for untagged
83
+ installs) simply never get flagged as outdated, which is the correct
84
+ behavior here."""
85
+ try:
86
+ return tuple(int(part) for part in value.split("."))
87
+ except ValueError:
88
+ return None
89
+
90
+
91
+ def _read_update_cache() -> dict:
92
+ if not UPDATE_CACHE_PATH.exists():
93
+ return {}
94
+ try:
95
+ return json.loads(UPDATE_CACHE_PATH.read_text(encoding="utf-8"))
96
+ except (OSError, ValueError):
97
+ return {}
98
+
99
+
100
+ def _write_update_cache(latest_version: str) -> None:
101
+ UPDATE_CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
102
+ payload = {"last_checked": time.time(), "latest_version": latest_version}
103
+ UPDATE_CACHE_PATH.write_text(json.dumps(payload), encoding="utf-8")
104
+
105
+
106
+ def _fetch_latest_version_from_pypi() -> str | None:
107
+ try:
108
+ url = f"https://pypi.org/pypi/{PACKAGE_NAME}/json"
109
+ with urllib.request.urlopen(url, timeout=UPDATE_CHECK_TIMEOUT_SECONDS) as response: # noqa: S310
110
+ payload = json.loads(response.read().decode("utf-8"))
111
+ return payload["info"]["version"]
112
+ except Exception:
113
+ return None
114
+
115
+
116
+ def _latest_known_version() -> str | None:
117
+ cache = _read_update_cache()
118
+ last_checked = cache.get("last_checked", 0)
119
+ if time.time() - last_checked < UPDATE_CHECK_INTERVAL_SECONDS:
120
+ return cache.get("latest_version")
121
+
122
+ # Update the cache timestamp even on a failed fetch, so an offline user
123
+ # doesn't pay the network timeout again on every single command - only
124
+ # once per interval.
125
+ latest = _fetch_latest_version_from_pypi()
126
+ _write_update_cache(latest or cache.get("latest_version", ""))
127
+ return latest or cache.get("latest_version")
128
+
129
+
130
+ def check_for_update() -> None:
131
+ """Prints a one-line notice to stderr if a newer aether-quant release is
132
+ available on PyPI. Never raises, never blocks a real command by more
133
+ than the short network timeout, and only actually checks PyPI once per
134
+ 24h (cached in ~/.aq/update_check.json). Opt out with
135
+ AQ_SKIP_UPDATE_CHECK=1 (e.g. for CI/scripted usage)."""
136
+ if os.environ.get("AQ_SKIP_UPDATE_CHECK"):
137
+ return
138
+ try:
139
+ installed = installed_version(PACKAGE_NAME)
140
+ latest = _latest_known_version()
141
+ if not latest:
142
+ return
143
+ installed_tuple = _parse_simple_version(installed)
144
+ latest_tuple = _parse_simple_version(latest)
145
+ if installed_tuple is None or latest_tuple is None:
146
+ return
147
+ if installed_tuple < latest_tuple:
148
+ print(
149
+ f"aq: a newer version is available ({latest}, you have {installed}) - "
150
+ f"upgrade with: pip install --upgrade {PACKAGE_NAME}",
151
+ file=sys.stderr,
152
+ )
153
+ except Exception:
154
+ pass
155
+
156
+
157
+ def cmd_train(args: argparse.Namespace) -> int:
158
+ cmd = [sys.executable, "train.py"]
159
+ if args.dataset_only:
160
+ cmd.append("--dataset-only")
161
+ elif args.init_only:
162
+ cmd.append("--init-only")
163
+ elif args.experts_only:
164
+ cmd.append("--experts-only")
165
+ return _run(cmd)
166
+
167
+
168
+ def cmd_test(_args: argparse.Namespace) -> int:
169
+ return _run([sys.executable, "-m", "pytest", "tests/"])
170
+
171
+
172
+ def cmd_backtest(_args: argparse.Namespace) -> int:
173
+ lean_binary = _find_quantconnect_lean_binary()
174
+ if lean_binary is None:
175
+ print("error: QuantConnect Lean CLI not found (checked .venv and PATH).", file=sys.stderr)
176
+ return 1
177
+ return _run([lean_binary, "backtest", "."])
178
+
179
+
180
+ def cmd_report(args: argparse.Namespace) -> int:
181
+ lean_binary = _find_quantconnect_lean_binary()
182
+ if lean_binary is None:
183
+ print("error: QuantConnect Lean CLI not found (checked .venv and PATH).", file=sys.stderr)
184
+ return 1
185
+ backtest_dir = ROOT_DIR / "backtests" / args.backtest_dir
186
+ return _run(
187
+ [
188
+ lean_binary,
189
+ "report",
190
+ "--backtest-results",
191
+ str(backtest_dir / f"{args.result_id}.json"),
192
+ "--report-destination",
193
+ str(backtest_dir / "report.html"),
194
+ "--overwrite",
195
+ ]
196
+ )
197
+
198
+
199
+ def cmd_api(_args: argparse.Namespace) -> int:
200
+ return _run([sys.executable, "-m", "uvicorn", "monitoring.api_server:app", "--port", "8001", "--reload"])
201
+
202
+
203
+ def cmd_webui(_args: argparse.Namespace) -> int:
204
+ npm = "npm.cmd" if sys.platform == "win32" else "npm"
205
+ return _run([npm, "run", "dev"], cwd=WEBUI_DIR)
206
+
207
+
208
+ def cmd_docker_up(args: argparse.Namespace) -> int:
209
+ if args.all:
210
+ services = [
211
+ "redis",
212
+ "postgres",
213
+ "aether-quant",
214
+ "experience-worker",
215
+ "performance-trigger-worker",
216
+ "retraining-worker",
217
+ "telegram-worker",
218
+ ]
219
+ return _run(["docker", "compose", "up", "-d", *services])
220
+ if args.lean:
221
+ return _run(["docker", "compose", "--profile", "lean", "up", "-d"])
222
+ return _run(["docker", "compose", "up", "-d", "redis", "postgres"])
223
+
224
+
225
+ def cmd_docker_build(_args: argparse.Namespace) -> int:
226
+ return _run(["docker", "compose", "build", "aether-quant"])
227
+
228
+
229
+ def cmd_retrain(args: argparse.Namespace) -> int:
230
+ return _run([sys.executable, "-m", "retraining.orchestrator", args.stage, *args.retrain_args])
231
+
232
+
233
+ def cmd_trade_lock(args: argparse.Namespace) -> int:
234
+ if args.on:
235
+ write_manual_trade_lock_override(True, CONFIG_PATH)
236
+ print("Trade lock override: ON (trading forced paused).")
237
+ elif args.off:
238
+ write_manual_trade_lock_override(False, CONFIG_PATH)
239
+ print("Trade lock override: OFF (trading forced resumed, even past a sticky total-drawdown lock).")
240
+ elif args.auto:
241
+ write_manual_trade_lock_override(None, CONFIG_PATH)
242
+ print("Trade lock override: AUTO (back to today's default automatic behavior).")
243
+ else: # status
244
+ override = read_manual_trade_lock_override(CONFIG_PATH)
245
+ label = {True: "ON (forced paused)", False: "OFF (forced resumed)", None: "AUTO (automatic behavior)"}[override]
246
+ print(f"Trade lock override: {label}")
247
+ return 0
248
+
249
+
250
+ def cmd_status(_args: argparse.Namespace) -> int:
251
+ return _run(["git", "status"])
252
+
253
+
254
+ def build_parser() -> argparse.ArgumentParser:
255
+ parser = argparse.ArgumentParser(prog="aq", description="Aether Quant convenience CLI")
256
+ subparsers = parser.add_subparsers(dest="command", required=True)
257
+
258
+ train_parser = subparsers.add_parser("train", help="Run the training pipeline (wraps python train.py)")
259
+ train_group = train_parser.add_mutually_exclusive_group()
260
+ train_group.add_argument("--dataset-only", action="store_true", help="Build dataset/scaler/manifest only")
261
+ train_group.add_argument("--init-only", action="store_true", help="Refresh the data inventory only")
262
+ train_group.add_argument("--experts-only", action="store_true", help="Train the 4 expert models only")
263
+ train_parser.set_defaults(func=cmd_train)
264
+
265
+ test_parser = subparsers.add_parser("test", help="Run the test suite (wraps pytest tests/)")
266
+ test_parser.set_defaults(func=cmd_test)
267
+
268
+ backtest_parser = subparsers.add_parser("backtest", help="Run a Lean backtest (wraps lean backtest .)")
269
+ backtest_parser.set_defaults(func=cmd_backtest)
270
+
271
+ report_parser = subparsers.add_parser("report", help="Generate a Lean HTML report for a finished backtest")
272
+ report_parser.add_argument("backtest_dir", help="Folder name under backtests/, e.g. 2026-07-04_13-06-51")
273
+ report_parser.add_argument("result_id", help="Result JSON id, e.g. 1366365999")
274
+ report_parser.set_defaults(func=cmd_report)
275
+
276
+ api_parser = subparsers.add_parser("api", help="Start the FastAPI monitoring server on :8001")
277
+ api_parser.set_defaults(func=cmd_api)
278
+
279
+ webui_parser = subparsers.add_parser("webui", help="Start the webui dev server (npm run dev)")
280
+ webui_parser.set_defaults(func=cmd_webui)
281
+
282
+ docker_parser = subparsers.add_parser("docker", help="Docker Compose shortcuts")
283
+ docker_subparsers = docker_parser.add_subparsers(dest="docker_command", required=True)
284
+
285
+ docker_up_parser = docker_subparsers.add_parser("up", help="Start infra services")
286
+ docker_up_group = docker_up_parser.add_mutually_exclusive_group()
287
+ docker_up_group.add_argument("--lean", action="store_true", help="Start via the lean Compose profile")
288
+ docker_up_group.add_argument("--all", action="store_true", help="Start the full stack, including all workers")
289
+ docker_up_parser.set_defaults(func=cmd_docker_up)
290
+
291
+ docker_build_parser = docker_subparsers.add_parser("build", help="Rebuild the aether-quant app image")
292
+ docker_build_parser.set_defaults(func=cmd_docker_build)
293
+
294
+ retrain_parser = subparsers.add_parser(
295
+ "retrain", help="Thin dispatcher to python -m retraining.orchestrator <stage> ..."
296
+ )
297
+ retrain_parser.add_argument(
298
+ "stage",
299
+ choices=["plan", "train", "train_topology", "validate", "backtest", "commit", "promote", "rollback", "status"],
300
+ )
301
+ retrain_parser.add_argument("retrain_args", nargs=argparse.REMAINDER, help="Passed through verbatim, e.g. --version-id <uuid>")
302
+ retrain_parser.set_defaults(func=cmd_retrain)
303
+
304
+ trade_lock_parser = subparsers.add_parser(
305
+ "trade-lock", help="Manually override the sticky total-drawdown trade lock"
306
+ )
307
+ trade_lock_group = trade_lock_parser.add_mutually_exclusive_group(required=True)
308
+ trade_lock_group.add_argument("--on", action="store_true", help="Force trading paused")
309
+ trade_lock_group.add_argument("--off", action="store_true", help="Force trading resumed")
310
+ trade_lock_group.add_argument("--auto", action="store_true", help="Return to fully automatic behavior")
311
+ trade_lock_group.add_argument("--status", dest="status", action="store_true", help="Print the current override state")
312
+ trade_lock_parser.set_defaults(func=cmd_trade_lock)
313
+
314
+ status_parser = subparsers.add_parser("status", help="Show git status")
315
+ status_parser.set_defaults(func=cmd_status)
316
+
317
+ return parser
318
+
319
+
320
+ def main() -> None:
321
+ parser = build_parser()
322
+ args = parser.parse_args()
323
+ exit_code = args.func(args)
324
+ check_for_update()
325
+ sys.exit(exit_code)
326
+
327
+
328
+ if __name__ == "__main__":
329
+ main()
risk/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # risk
2
+
3
+ Owns V2 dynamic risk controls:
4
+
5
+ - volatility-adjusted position sizing
6
+ - leverage caps
7
+ - drawdown-aware sizing
8
+ - liquidity checks
9
+ - market-impact and slippage controls
10
+
11
+ This package should reuse the existing conservative risk-control behavior and extend it gradually.
12
+
13
+ Current V2-7 behavior:
14
+
15
+ - classifies rolling volatility into low, normal and high regimes
16
+ - scales target position weight toward a target daily volatility
17
+ - reduces exposure in high-volatility regimes
18
+ - allows controlled expansion in low-volatility regimes
19
+ - emits leverage/sizing telemetry for the future HTML volatility dashboard
risk/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ """Dynamic risk, leverage and liquidity controls for Aether Quant V2."""
2
+
3
+ from .manual_override import read_manual_trade_lock_override, write_manual_trade_lock_override
4
+ from .position_sizing import (
5
+ PositionSizingDecision,
6
+ build_dynamic_position_sizing,
7
+ classify_volatility_regime,
8
+ )
9
+
10
+ __all__ = [
11
+ "PositionSizingDecision",
12
+ "build_dynamic_position_sizing",
13
+ "classify_volatility_regime",
14
+ "read_manual_trade_lock_override",
15
+ "write_manual_trade_lock_override",
16
+ ]
@@ -0,0 +1,61 @@
1
+ """Manual trade-lock override for Aether Quant V2.
2
+
3
+ `main.py`'s sticky total-drawdown lock (see `_refresh_risk_state()`) never
4
+ auto-clears on its own once tripped - by design, a capital-preservation
5
+ circuit breaker requiring a deliberate decision to resume trading. This
6
+ module is that deliberate decision, expressed as a single config value:
7
+ `phase_v2.risk.manual_trade_lock_override` (`true` = force-lock, `false` =
8
+ force-clear, absent/`null` = leave today's automatic behavior untouched).
9
+
10
+ Two callers, both read-modify-write the same key:
11
+ - `aq_cli.py`'s `trade-lock` command - a human flips the switch directly.
12
+ - `retraining/orchestrator.py::promote()` - a successful promotion clears it
13
+ automatically, tying "trading resumes" to "a genuinely new model shipped".
14
+
15
+ This is the first place in the codebase where a Python process writes to
16
+ `config.json` (every other reader treats it as human-edited, read-only
17
+ input) - a deliberate, narrow exception, not a new general pattern. It is a
18
+ *standing* switch, not one-shot: it stays in effect until explicitly changed
19
+ again, so `main.py` itself never writes back to `config.json` - it only
20
+ reads this one key once per session rollover (see `_refresh_risk_state()`).
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ from pathlib import Path
27
+
28
+ _PHASE_V2_KEY = "phase_v2"
29
+ _RISK_KEY = "risk"
30
+ _OVERRIDE_KEY = "manual_trade_lock_override"
31
+
32
+
33
+ def read_manual_trade_lock_override(config_path: Path) -> bool | None:
34
+ """Returns True/False if a manual override is set, None if absent/unset
35
+ or the config file doesn't exist yet - never raises."""
36
+ if not config_path.exists():
37
+ return None
38
+ with config_path.open("r", encoding="utf-8") as f:
39
+ config = json.load(f)
40
+ value = config.get(_PHASE_V2_KEY, {}).get(_RISK_KEY, {}).get(_OVERRIDE_KEY)
41
+ return value if isinstance(value, bool) else None
42
+
43
+
44
+ def write_manual_trade_lock_override(value: bool | None, config_path: Path) -> None:
45
+ """Read-modify-write config.json, setting only
46
+ phase_v2.risk.manual_trade_lock_override - every other key is preserved
47
+ untouched. value=None removes the override key entirely (returns to
48
+ fully automatic behavior)."""
49
+ with config_path.open("r", encoding="utf-8") as f:
50
+ config = json.load(f)
51
+
52
+ phase_v2 = config.setdefault(_PHASE_V2_KEY, {})
53
+ risk = phase_v2.setdefault(_RISK_KEY, {})
54
+ if value is None:
55
+ risk.pop(_OVERRIDE_KEY, None)
56
+ else:
57
+ risk[_OVERRIDE_KEY] = value
58
+
59
+ with config_path.open("w", encoding="utf-8") as f:
60
+ json.dump(config, f, indent=4)
61
+ f.write("\n")
@@ -0,0 +1,113 @@
1
+ """Dynamic position sizing for Aether Quant V2."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from dataclasses import asdict, dataclass
7
+
8
+
9
+ TRADING_DAYS_PER_YEAR = 252
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class PositionSizingDecision:
14
+ base_target_weight: float
15
+ target_weight: float
16
+ rolling_volatility: float
17
+ annualized_volatility: float
18
+ volatility_regime: str
19
+ volatility_multiplier: float
20
+ confidence_multiplier: float
21
+ leverage_factor: float
22
+ max_leverage: float
23
+ sizing_reason: str
24
+
25
+ def to_dict(self) -> dict:
26
+ return asdict(self)
27
+
28
+
29
+ def classify_volatility_regime(
30
+ rolling_volatility: float,
31
+ low_volatility_threshold: float,
32
+ high_volatility_threshold: float,
33
+ ) -> str:
34
+ volatility = abs(float(rolling_volatility))
35
+ if volatility >= high_volatility_threshold:
36
+ return "high_volatility"
37
+ if volatility <= low_volatility_threshold:
38
+ return "low_volatility"
39
+ return "normal_volatility"
40
+
41
+
42
+ def build_dynamic_position_sizing(
43
+ base_target_weight: float,
44
+ confidence: float,
45
+ rolling_volatility: float,
46
+ max_position_weight: float,
47
+ target_daily_volatility: float = 0.015,
48
+ min_position_weight: float = 0.0,
49
+ low_volatility_threshold: float = 0.01,
50
+ high_volatility_threshold: float = 0.03,
51
+ min_volatility_multiplier: float = 0.35,
52
+ max_volatility_multiplier: float = 1.25,
53
+ max_leverage: float = 1.0,
54
+ ) -> PositionSizingDecision:
55
+ base_target_weight = float(base_target_weight)
56
+ confidence = max(0.0, min(float(confidence), 1.0))
57
+ max_position_weight = max(float(max_position_weight), 0.0)
58
+ abs_base_target = abs(base_target_weight)
59
+ volatility = abs(float(rolling_volatility))
60
+ annualized_volatility = volatility * math.sqrt(TRADING_DAYS_PER_YEAR)
61
+ volatility_regime = classify_volatility_regime(
62
+ volatility,
63
+ low_volatility_threshold,
64
+ high_volatility_threshold,
65
+ )
66
+
67
+ if abs_base_target == 0.0 or confidence == 0.0 or max_position_weight == 0.0:
68
+ return PositionSizingDecision(
69
+ base_target_weight=base_target_weight,
70
+ target_weight=0.0,
71
+ rolling_volatility=volatility,
72
+ annualized_volatility=annualized_volatility,
73
+ volatility_regime=volatility_regime,
74
+ volatility_multiplier=0.0,
75
+ confidence_multiplier=0.0,
76
+ leverage_factor=0.0,
77
+ max_leverage=float(max_leverage),
78
+ sizing_reason="no_active_signal",
79
+ )
80
+
81
+ safe_volatility = max(volatility, 1e-6)
82
+ volatility_multiplier = target_daily_volatility / safe_volatility
83
+ volatility_multiplier = max(min_volatility_multiplier, min(volatility_multiplier, max_volatility_multiplier))
84
+ confidence_multiplier = 0.5 + 0.5 * confidence
85
+
86
+ sized_weight = abs_base_target * volatility_multiplier * confidence_multiplier
87
+ sized_weight = min(sized_weight, max_position_weight)
88
+ if sized_weight > 0.0 and min_position_weight > 0.0:
89
+ sized_weight = max(sized_weight, min_position_weight)
90
+
91
+ direction = 1.0 if base_target_weight >= 0.0 else -1.0
92
+ target_weight = direction * sized_weight
93
+ leverage_factor = min(float(max_leverage), sized_weight / max(abs_base_target, 1e-9))
94
+
95
+ if volatility_regime == "high_volatility":
96
+ reason = "reduced_for_high_volatility"
97
+ elif volatility_regime == "low_volatility" and volatility_multiplier > 1.0:
98
+ reason = "expanded_for_low_volatility"
99
+ else:
100
+ reason = "normal_volatility_sizing"
101
+
102
+ return PositionSizingDecision(
103
+ base_target_weight=base_target_weight,
104
+ target_weight=target_weight,
105
+ rolling_volatility=volatility,
106
+ annualized_volatility=annualized_volatility,
107
+ volatility_regime=volatility_regime,
108
+ volatility_multiplier=volatility_multiplier,
109
+ confidence_multiplier=confidence_multiplier,
110
+ leverage_factor=leverage_factor,
111
+ max_leverage=float(max_leverage),
112
+ sizing_reason=reason,
113
+ )