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.
- aether_quant-0.2.0.dist-info/METADATA +5 -0
- aether_quant-0.2.0.dist-info/RECORD +12 -0
- aether_quant-0.2.0.dist-info/WHEEL +5 -0
- aether_quant-0.2.0.dist-info/entry_points.txt +2 -0
- aether_quant-0.2.0.dist-info/scm_file_list.json +211 -0
- aether_quant-0.2.0.dist-info/scm_version.json +8 -0
- aether_quant-0.2.0.dist-info/top_level.txt +2 -0
- aq_cli.py +329 -0
- risk/README.md +19 -0
- risk/__init__.py +16 -0
- risk/manual_override.py +61 -0
- risk/position_sizing.py +113 -0
|
@@ -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,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
|
+
}
|
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
|
+
]
|
risk/manual_override.py
ADDED
|
@@ -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")
|
risk/position_sizing.py
ADDED
|
@@ -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
|
+
)
|