aether-quant 0.5.0__tar.gz → 0.6.0__tar.gz

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.
Files changed (248) hide show
  1. {aether_quant-0.5.0 → aether_quant-0.6.0}/.gitignore +0 -2
  2. {aether_quant-0.5.0 → aether_quant-0.6.0}/PKG-INFO +1 -1
  3. {aether_quant-0.5.0 → aether_quant-0.6.0}/README.md +18 -2
  4. {aether_quant-0.5.0 → aether_quant-0.6.0}/aether_quant.egg-info/PKG-INFO +1 -1
  5. {aether_quant-0.5.0 → aether_quant-0.6.0}/aether_quant.egg-info/SOURCES.txt +2 -0
  6. {aether_quant-0.5.0 → aether_quant-0.6.0}/aether_quant.egg-info/scm_file_list.json +2 -0
  7. {aether_quant-0.5.0 → aether_quant-0.6.0}/aether_quant.egg-info/scm_version.json +2 -2
  8. {aether_quant-0.5.0 → aether_quant-0.6.0}/analyzer/README.md +26 -0
  9. {aether_quant-0.5.0 → aether_quant-0.6.0}/analyzer/__init__.py +2 -0
  10. {aether_quant-0.5.0 → aether_quant-0.6.0}/analyzer/market_analyzer.py +81 -2
  11. {aether_quant-0.5.0 → aether_quant-0.6.0}/aq_cli.py +151 -1
  12. aether_quant-0.6.0/config.json +543 -0
  13. {aether_quant-0.5.0 → aether_quant-0.6.0}/development/Changelog.md +76 -0
  14. aether_quant-0.6.0/lean.json +649 -0
  15. {aether_quant-0.5.0 → aether_quant-0.6.0}/main.py +2 -0
  16. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_aq_cli.py +234 -0
  17. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_market_analyzer.py +118 -1
  18. {aether_quant-0.5.0 → aether_quant-0.6.0}/.dockerignore +0 -0
  19. {aether_quant-0.5.0 → aether_quant-0.6.0}/.env.compose.example +0 -0
  20. {aether_quant-0.5.0 → aether_quant-0.6.0}/.env.live.example +0 -0
  21. {aether_quant-0.5.0 → aether_quant-0.6.0}/.gitattributes +0 -0
  22. {aether_quant-0.5.0 → aether_quant-0.6.0}/.github/workflows/ci.yml +0 -0
  23. {aether_quant-0.5.0 → aether_quant-0.6.0}/.github/workflows/release.yml +0 -0
  24. {aether_quant-0.5.0 → aether_quant-0.6.0}/Dockerfile +0 -0
  25. {aether_quant-0.5.0 → aether_quant-0.6.0}/Dockerfile.retraining_worker +0 -0
  26. {aether_quant-0.5.0 → aether_quant-0.6.0}/Dockerfile.workers +0 -0
  27. {aether_quant-0.5.0 → aether_quant-0.6.0}/aether_quant.egg-info/dependency_links.txt +0 -0
  28. {aether_quant-0.5.0 → aether_quant-0.6.0}/aether_quant.egg-info/entry_points.txt +0 -0
  29. {aether_quant-0.5.0 → aether_quant-0.6.0}/aether_quant.egg-info/top_level.txt +0 -0
  30. {aether_quant-0.5.0 → aether_quant-0.6.0}/backtests/.gitkeep +0 -0
  31. {aether_quant-0.5.0 → aether_quant-0.6.0}/data/alternative/estimize/consensus/aapl/.keep +0 -0
  32. {aether_quant-0.5.0 → aether_quant-0.6.0}/data/alternative/estimize/estimate/.keep +0 -0
  33. {aether_quant-0.5.0 → aether_quant-0.6.0}/data/alternative/estimize/release/.keep +0 -0
  34. {aether_quant-0.5.0 → aether_quant-0.6.0}/data/alternative/sec/aapl/.keep +0 -0
  35. {aether_quant-0.5.0 → aether_quant-0.6.0}/data/alternative/trading-economics/.keep +0 -0
  36. {aether_quant-0.5.0 → aether_quant-0.6.0}/data/alternative/trading-economics/calendar/.keep +0 -0
  37. {aether_quant-0.5.0 → aether_quant-0.6.0}/data/alternative/trading-economics/earnings/.keep +0 -0
  38. {aether_quant-0.5.0 → aether_quant-0.6.0}/data/alternative/trading-economics/indicator/.keep +0 -0
  39. {aether_quant-0.5.0 → aether_quant-0.6.0}/data_pipeline/README.md +0 -0
  40. {aether_quant-0.5.0 → aether_quant-0.6.0}/data_pipeline/__init__.py +0 -0
  41. {aether_quant-0.5.0 → aether_quant-0.6.0}/data_pipeline/fetch.py +0 -0
  42. {aether_quant-0.5.0 → aether_quant-0.6.0}/data_pipeline/v2_manifest.py +0 -0
  43. {aether_quant-0.5.0 → aether_quant-0.6.0}/data_pipeline/yfinance_backfill.py +0 -0
  44. {aether_quant-0.5.0 → aether_quant-0.6.0}/development/Problems.md +0 -0
  45. {aether_quant-0.5.0 → aether_quant-0.6.0}/development/README.md +0 -0
  46. {aether_quant-0.5.0 → aether_quant-0.6.0}/development/backtest_equity_chart.png +0 -0
  47. {aether_quant-0.5.0 → aether_quant-0.6.0}/development/infrastructure.md +0 -0
  48. {aether_quant-0.5.0 → aether_quant-0.6.0}/development/logo.png +0 -0
  49. {aether_quant-0.5.0 → aether_quant-0.6.0}/development/v2_architecture.md +0 -0
  50. {aether_quant-0.5.0 → aether_quant-0.6.0}/docker-compose.yml +0 -0
  51. {aether_quant-0.5.0 → aether_quant-0.6.0}/execution/README.md +0 -0
  52. {aether_quant-0.5.0 → aether_quant-0.6.0}/execution/__init__.py +0 -0
  53. {aether_quant-0.5.0 → aether_quant-0.6.0}/execution/config_cache.py +0 -0
  54. {aether_quant-0.5.0 → aether_quant-0.6.0}/execution/live_credentials.py +0 -0
  55. {aether_quant-0.5.0 → aether_quant-0.6.0}/execution/live_credentials_io.py +0 -0
  56. {aether_quant-0.5.0 → aether_quant-0.6.0}/execution/order_gate.py +0 -0
  57. {aether_quant-0.5.0 → aether_quant-0.6.0}/execution/paper_readiness.py +0 -0
  58. {aether_quant-0.5.0 → aether_quant-0.6.0}/execution/paper_readiness_io.py +0 -0
  59. {aether_quant-0.5.0 → aether_quant-0.6.0}/execution/paper_readiness_report.py +0 -0
  60. {aether_quant-0.5.0 → aether_quant-0.6.0}/execution/runtime_config_io.py +0 -0
  61. {aether_quant-0.5.0 → aether_quant-0.6.0}/experience/README.md +0 -0
  62. {aether_quant-0.5.0 → aether_quant-0.6.0}/experience/__init__.py +0 -0
  63. {aether_quant-0.5.0 → aether_quant-0.6.0}/experience/observation_metrics.py +0 -0
  64. {aether_quant-0.5.0 → aether_quant-0.6.0}/experience/postgres_worker.py +0 -0
  65. {aether_quant-0.5.0 → aether_quant-0.6.0}/experience/redis_queue.py +0 -0
  66. {aether_quant-0.5.0 → aether_quant-0.6.0}/experience/simulated_portfolio.py +0 -0
  67. {aether_quant-0.5.0 → aether_quant-0.6.0}/experts/README.md +0 -0
  68. {aether_quant-0.5.0 → aether_quant-0.6.0}/experts/__init__.py +0 -0
  69. {aether_quant-0.5.0 → aether_quant-0.6.0}/experts/expert_datasets.py +0 -0
  70. {aether_quant-0.5.0 → aether_quant-0.6.0}/generate_backtest_report.py +0 -0
  71. {aether_quant-0.5.0 → aether_quant-0.6.0}/inference/README.md +0 -0
  72. {aether_quant-0.5.0 → aether_quant-0.6.0}/inference/__init__.py +0 -0
  73. {aether_quant-0.5.0 → aether_quant-0.6.0}/inference/exported_model.py +0 -0
  74. {aether_quant-0.5.0 → aether_quant-0.6.0}/liquidity/README.md +0 -0
  75. {aether_quant-0.5.0 → aether_quant-0.6.0}/liquidity/__init__.py +0 -0
  76. {aether_quant-0.5.0 → aether_quant-0.6.0}/liquidity/market_liquidity.py +0 -0
  77. {aether_quant-0.5.0 → aether_quant-0.6.0}/ml/README.md +0 -0
  78. {aether_quant-0.5.0 → aether_quant-0.6.0}/ml/gating_feature_schema.json +0 -0
  79. {aether_quant-0.5.0 → aether_quant-0.6.0}/ml/gating_model.json +0 -0
  80. {aether_quant-0.5.0 → aether_quant-0.6.0}/ml/gating_training_metrics.json +0 -0
  81. {aether_quant-0.5.0 → aether_quant-0.6.0}/ml/versions/gating-only-26218402-34d9-4a0a-add8-ee5d50dd0459/gating_feature_schema.json +0 -0
  82. {aether_quant-0.5.0 → aether_quant-0.6.0}/ml/versions/gating-only-26218402-34d9-4a0a-add8-ee5d50dd0459/gating_model.json +0 -0
  83. {aether_quant-0.5.0 → aether_quant-0.6.0}/ml/versions/gating-only-26218402-34d9-4a0a-add8-ee5d50dd0459/gating_training_metrics.json +0 -0
  84. {aether_quant-0.5.0 → aether_quant-0.6.0}/moe/README.md +0 -0
  85. {aether_quant-0.5.0 → aether_quant-0.6.0}/moe/__init__.py +0 -0
  86. {aether_quant-0.5.0 → aether_quant-0.6.0}/moe/gating.py +0 -0
  87. {aether_quant-0.5.0 → aether_quant-0.6.0}/monitoring/README.md +0 -0
  88. {aether_quant-0.5.0 → aether_quant-0.6.0}/monitoring/__init__.py +0 -0
  89. {aether_quant-0.5.0 → aether_quant-0.6.0}/monitoring/api_server.py +0 -0
  90. {aether_quant-0.5.0 → aether_quant-0.6.0}/monitoring/neural_network_state.py +0 -0
  91. {aether_quant-0.5.0 → aether_quant-0.6.0}/notifications/README.md +0 -0
  92. {aether_quant-0.5.0 → aether_quant-0.6.0}/notifications/__init__.py +0 -0
  93. {aether_quant-0.5.0 → aether_quant-0.6.0}/notifications/postgres_telegram.py +0 -0
  94. {aether_quant-0.5.0 → aether_quant-0.6.0}/notifications/telegram_alerts.py +0 -0
  95. {aether_quant-0.5.0 → aether_quant-0.6.0}/notifications/telegram_client.py +0 -0
  96. {aether_quant-0.5.0 → aether_quant-0.6.0}/notifications/telegram_worker.py +0 -0
  97. {aether_quant-0.5.0 → aether_quant-0.6.0}/performance/README.md +0 -0
  98. {aether_quant-0.5.0 → aether_quant-0.6.0}/performance/__init__.py +0 -0
  99. {aether_quant-0.5.0 → aether_quant-0.6.0}/performance/postgres_triggers.py +0 -0
  100. {aether_quant-0.5.0 → aether_quant-0.6.0}/performance/trigger_worker.py +0 -0
  101. {aether_quant-0.5.0 → aether_quant-0.6.0}/performance/triggers.py +0 -0
  102. {aether_quant-0.5.0 → aether_quant-0.6.0}/pyproject.toml +0 -0
  103. {aether_quant-0.5.0 → aether_quant-0.6.0}/regime/README.md +0 -0
  104. {aether_quant-0.5.0 → aether_quant-0.6.0}/regime/__init__.py +0 -0
  105. {aether_quant-0.5.0 → aether_quant-0.6.0}/regime/market_regime.py +0 -0
  106. {aether_quant-0.5.0 → aether_quant-0.6.0}/requirements/README.md +0 -0
  107. {aether_quant-0.5.0 → aether_quant-0.6.0}/requirements/requirements-dev.txt +0 -0
  108. {aether_quant-0.5.0 → aether_quant-0.6.0}/requirements/requirements-retraining-worker.txt +0 -0
  109. {aether_quant-0.5.0 → aether_quant-0.6.0}/requirements/requirements-runtime.txt +0 -0
  110. {aether_quant-0.5.0 → aether_quant-0.6.0}/requirements/requirements-workers.txt +0 -0
  111. {aether_quant-0.5.0 → aether_quant-0.6.0}/requirements/requirements.txt +0 -0
  112. {aether_quant-0.5.0 → aether_quant-0.6.0}/retraining/README.md +0 -0
  113. {aether_quant-0.5.0 → aether_quant-0.6.0}/retraining/__init__.py +0 -0
  114. {aether_quant-0.5.0 → aether_quant-0.6.0}/retraining/artifacts.py +0 -0
  115. {aether_quant-0.5.0 → aether_quant-0.6.0}/retraining/backtest_gate.py +0 -0
  116. {aether_quant-0.5.0 → aether_quant-0.6.0}/retraining/lean_backtest.py +0 -0
  117. {aether_quant-0.5.0 → aether_quant-0.6.0}/retraining/orchestrator.py +0 -0
  118. {aether_quant-0.5.0 → aether_quant-0.6.0}/retraining/planning.py +0 -0
  119. {aether_quant-0.5.0 → aether_quant-0.6.0}/retraining/postgres_registry.py +0 -0
  120. {aether_quant-0.5.0 → aether_quant-0.6.0}/retraining/status_export.py +0 -0
  121. {aether_quant-0.5.0 → aether_quant-0.6.0}/retraining/validation_gate.py +0 -0
  122. {aether_quant-0.5.0 → aether_quant-0.6.0}/retraining/vault_client.py +0 -0
  123. {aether_quant-0.5.0 → aether_quant-0.6.0}/retraining/vault_commands.py +0 -0
  124. {aether_quant-0.5.0 → aether_quant-0.6.0}/retraining/worker.py +0 -0
  125. {aether_quant-0.5.0 → aether_quant-0.6.0}/risk/README.md +0 -0
  126. {aether_quant-0.5.0 → aether_quant-0.6.0}/risk/__init__.py +0 -0
  127. {aether_quant-0.5.0 → aether_quant-0.6.0}/risk/manual_override.py +0 -0
  128. {aether_quant-0.5.0 → aether_quant-0.6.0}/risk/position_sizing.py +0 -0
  129. {aether_quant-0.5.0 → aether_quant-0.6.0}/risk_controls.py +0 -0
  130. {aether_quant-0.5.0 → aether_quant-0.6.0}/setup.cfg +0 -0
  131. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/README.md +0 -0
  132. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_backtest_gate.py +0 -0
  133. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_config_cache.py +0 -0
  134. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_experience_queue.py +0 -0
  135. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_expert_datasets.py +0 -0
  136. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_expert_models.py +0 -0
  137. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_exported_model.py +0 -0
  138. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_fetch.py +0 -0
  139. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_gating_network.py +0 -0
  140. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_generate_backtest_report.py +0 -0
  141. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_lean_backtest.py +0 -0
  142. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_lean_backtest_ml_coverage.py +0 -0
  143. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_learned_topology.py +0 -0
  144. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_live_credentials.py +0 -0
  145. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_live_credentials_io.py +0 -0
  146. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_manual_override.py +0 -0
  147. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_market_liquidity.py +0 -0
  148. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_market_regime.py +0 -0
  149. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_market_topology.py +0 -0
  150. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_neural_network_state.py +0 -0
  151. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_observation_metrics.py +0 -0
  152. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_order_gate.py +0 -0
  153. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_paper_readiness.py +0 -0
  154. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_paper_readiness_io.py +0 -0
  155. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_paper_readiness_report.py +0 -0
  156. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_position_sizing.py +0 -0
  157. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_postgres_telegram.py +0 -0
  158. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_postgres_triggers.py +0 -0
  159. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_postgres_worker.py +0 -0
  160. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_retraining_artifacts.py +0 -0
  161. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_retraining_orchestrator.py +0 -0
  162. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_retraining_planning.py +0 -0
  163. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_retraining_postgres_registry.py +0 -0
  164. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_retraining_worker.py +0 -0
  165. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_risk_controls.py +0 -0
  166. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_runtime_config_io.py +0 -0
  167. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_simulated_portfolio.py +0 -0
  168. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_status_export.py +0 -0
  169. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_telegram_alerts.py +0 -0
  170. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_telegram_client.py +0 -0
  171. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_telegram_worker.py +0 -0
  172. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_train_gating.py +0 -0
  173. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_train_pipeline.py +0 -0
  174. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_train_topology.py +0 -0
  175. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_trigger_worker.py +0 -0
  176. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_triggers.py +0 -0
  177. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_v2_pipeline_manifest.py +0 -0
  178. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_validation_gate.py +0 -0
  179. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_vault_client.py +0 -0
  180. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_vault_commands.py +0 -0
  181. {aether_quant-0.5.0 → aether_quant-0.6.0}/tests/test_yfinance_backfill.py +0 -0
  182. {aether_quant-0.5.0 → aether_quant-0.6.0}/topology/README.md +0 -0
  183. {aether_quant-0.5.0 → aether_quant-0.6.0}/topology/__init__.py +0 -0
  184. {aether_quant-0.5.0 → aether_quant-0.6.0}/topology/learned_topology.py +0 -0
  185. {aether_quant-0.5.0 → aether_quant-0.6.0}/topology/market_topology.py +0 -0
  186. {aether_quant-0.5.0 → aether_quant-0.6.0}/train.py +0 -0
  187. {aether_quant-0.5.0 → aether_quant-0.6.0}/train_gating.py +0 -0
  188. {aether_quant-0.5.0 → aether_quant-0.6.0}/train_topology.py +0 -0
  189. {aether_quant-0.5.0 → aether_quant-0.6.0}/visualization/README.md +0 -0
  190. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/.gitignore +0 -0
  191. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/.oxlintrc.json +0 -0
  192. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/README.md +0 -0
  193. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/index.html +0 -0
  194. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/overview_v2_12.png +0 -0
  195. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/package-lock.json +0 -0
  196. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/package.json +0 -0
  197. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/public/favicon.svg +0 -0
  198. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/public/icons.svg +0 -0
  199. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/risk_v2_12.png +0 -0
  200. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/App.tsx +0 -0
  201. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/api/client.ts +0 -0
  202. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/api/hooks.ts +0 -0
  203. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/heatmap/AssetHeatmap.tsx +0 -0
  204. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/layout/AppShell.tsx +0 -0
  205. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/layout/Panel.tsx +0 -0
  206. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/monitoring/CountTable.tsx +0 -0
  207. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/monitoring/MonitoringFeeds.tsx +0 -0
  208. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/monitoring/ObservationPanel.tsx +0 -0
  209. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/monitoring/PaperReadinessPanel.tsx +0 -0
  210. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/monitoring/PerformanceTriggersPanel.tsx +0 -0
  211. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/monitoring/RawStateViewer.tsx +0 -0
  212. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/monitoring/RetrainingStatusPanel.tsx +0 -0
  213. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/neuralnet/NeuralNetworkScene3D.tsx +0 -0
  214. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/neuralnet/NeuralNetworkStatsPanel.tsx +0 -0
  215. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/risk/AssetSizingTable.tsx +0 -0
  216. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/risk/LiquidityTable.tsx +0 -0
  217. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/risk/RiskBar.tsx +0 -0
  218. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/risk/RiskCore.tsx +0 -0
  219. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/risk/StrategyRiskCards.tsx +0 -0
  220. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/scene3d/Scene3D.tsx +0 -0
  221. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/scorecards/Scorecards.tsx +0 -0
  222. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/signals/Badge.tsx +0 -0
  223. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/signals/PositionsList.tsx +0 -0
  224. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/signals/SignalBoard.tsx +0 -0
  225. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/topology/ClusterList.tsx +0 -0
  226. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/topology/TopologyLearningPanel.tsx +0 -0
  227. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/topology/TopologyScene3D.tsx +0 -0
  228. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/tracing/AssetPerformancePanel.tsx +0 -0
  229. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/tracing/BacktestEquityPanel.tsx +0 -0
  230. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/tracing/DivergingBarChart.tsx +0 -0
  231. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/tracing/LineChart.tsx +0 -0
  232. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/tracing/MetricsSnapshotPanel.tsx +0 -0
  233. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/components/tracing/ObservationEquityPanel.tsx +0 -0
  234. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/index.css +0 -0
  235. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/lib/downsample.ts +0 -0
  236. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/lib/format.ts +0 -0
  237. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/main.tsx +0 -0
  238. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/pages/NeuralNetworkPage.tsx +0 -0
  239. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/pages/Overview.tsx +0 -0
  240. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/pages/RiskPage.tsx +0 -0
  241. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/pages/TopologyPage.tsx +0 -0
  242. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/pages/TracingPage.tsx +0 -0
  243. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/types/state.ts +0 -0
  244. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/src/types/tracing.ts +0 -0
  245. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/tsconfig.app.json +0 -0
  246. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/tsconfig.json +0 -0
  247. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/tsconfig.node.json +0 -0
  248. {aether_quant-0.5.0 → aether_quant-0.6.0}/webui/vite.config.ts +0 -0
@@ -19,9 +19,7 @@ env/
19
19
  !.env.live.example
20
20
  ib_config.py
21
21
  *_config.local.py
22
- config.json
23
22
  config.local.json
24
- lean.json
25
23
  lean.local.json
26
24
 
27
25
  *.key
@@ -1,5 +1,5 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aether-quant
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Aether Quant convenience CLI
5
5
  Requires-Python: >=3.10
@@ -10,11 +10,23 @@
10
10
 
11
11
  <p align="center">
12
12
  <img src="https://img.shields.io/badge/python-3.10%2B-FF8C00?style=flat-square&labelColor=1A1A1A&logo=python&logoColor=white" alt="Python 3.10+">
13
- <!-- AQ:TEST_BADGE_START --><img src="https://img.shields.io/badge/tests-583%2F583%20passing-brightgreen?style=flat-square&labelColor=1A1A1A" alt="583 of 583 tests passing"><!-- AQ:TEST_BADGE_END -->
13
+ <!-- AQ:TEST_BADGE_START --><img src="https://img.shields.io/badge/tests-611%2F611%20passing-brightgreen?style=flat-square&labelColor=1A1A1A" alt="611 of 611 tests passing"><!-- AQ:TEST_BADGE_END -->
14
14
  <img src="https://img.shields.io/pypi/v/aether-quant?style=flat-square&labelColor=1A1A1A&color=FF8C00" alt="PyPI version">
15
15
  <img src="https://img.shields.io/badge/docker-ghcr.io%2Faether--quant-2496ED?style=flat-square&labelColor=1A1A1A&logo=docker&logoColor=white" alt="Docker image on GHCR">
16
16
  </p>
17
17
 
18
+ <p align="center">
19
+ <img src="https://img.shields.io/badge/PyTorch-4B5563?style=flat-square&labelColor=1A1A1A&logo=pytorch&logoColor=white" alt="PyTorch">
20
+ <img src="https://img.shields.io/badge/scikit--learn-4B5563?style=flat-square&labelColor=1A1A1A&logo=scikitlearn&logoColor=white" alt="scikit-learn">
21
+ <img src="https://img.shields.io/badge/QuantConnect%20Lean-4B5563?style=flat-square&labelColor=1A1A1A" alt="QuantConnect Lean">
22
+ <img src="https://img.shields.io/badge/FastAPI-4B5563?style=flat-square&labelColor=1A1A1A&logo=fastapi&logoColor=white" alt="FastAPI">
23
+ <img src="https://img.shields.io/badge/React-4B5563?style=flat-square&labelColor=1A1A1A&logo=react&logoColor=white" alt="React">
24
+ <img src="https://img.shields.io/badge/TypeScript-4B5563?style=flat-square&labelColor=1A1A1A&logo=typescript&logoColor=white" alt="TypeScript">
25
+ <img src="https://img.shields.io/badge/Redis-4B5563?style=flat-square&labelColor=1A1A1A&logo=redis&logoColor=white" alt="Redis">
26
+ <img src="https://img.shields.io/badge/PostgreSQL-4B5563?style=flat-square&labelColor=1A1A1A&logo=postgresql&logoColor=white" alt="PostgreSQL">
27
+ <img src="https://img.shields.io/badge/GitHub%20Actions-4B5563?style=flat-square&labelColor=1A1A1A&logo=githubactions&logoColor=white" alt="GitHub Actions">
28
+ </p>
29
+
18
30
  Aether Quant is not a single static strategy — it's a **dynamic system**: a
19
31
  Mixture-of-Experts ensemble (bullish/bearish/sideways/volatility specialists)
20
32
  routed by a learned gating network, a market-regime detector, a 3D market
@@ -514,6 +526,8 @@ aq api
514
526
  aq webui
515
527
  aq docker up [--lean|--all]
516
528
  aq docker build
529
+ aq config [get <dotted.key>|set <dotted.key> <value>|keys [<dotted.prefix>]]
530
+ aq lean [get <dotted.key>|set <dotted.key> <value>|keys [<dotted.prefix>]]
517
531
  aq retrain <plan|train|validate|backtest|commit|promote|rollback|status> [...]
518
532
  aq trade-lock --on|--off|--auto|--status
519
533
  aq fetch <crypto|stock> --ticker <TICKER> --start <YYYY-MM-DD> --end <YYYY-MM-DD> [--apply]
@@ -531,6 +545,8 @@ wrapper around a command already documented elsewhere in this README:
531
545
  - **`aq webui`** — starts the webui dev server (`npm run dev`).
532
546
  - **`aq docker up [--lean|--all]`** — starts local infrastructure (default: Redis + PostgreSQL only).
533
547
  - **`aq docker build`** — rebuilds the `aether-quant` app image.
548
+ - **`aq config`** — reads or edits `config.json` directly, no manual file editing needed. Bare `aq config` pretty-prints the whole file; `aq config keys [<dotted.prefix>]` lists every leaf key path (handy for finding the right key in a deeply nested file); `aq config get <dotted.key>` prints one value (scalar, or a whole nested section as JSON); `aq config set <dotted.key> <value>` writes it — the value is parsed as JSON first (so `true`/`123`/`0.5`/`["a","b"]` become their real types automatically), falling back to a plain string otherwise. Every `set` backs up the previous file to `config.json.bak` first and prints old → new so a mistake is immediately visible; changing a value's type (e.g. bool → string) prints a warning but still writes it, since this command intentionally gives full access to every key, not just a safe subset.
549
+ - **`aq lean`** — the exact same `get`/`set`/`keys` tool as `aq config`, just pointed at `lean.json` (the QuantConnect Lean CLI's own config file — broker credentials, environments, data providers) instead. `aq lean set ib-trading-mode live`, `aq lean keys environments.live-paper`, etc.
534
550
  - **`aq retrain <stage>`** — dispatches to `python -m retraining.orchestrator <stage> ...` for a single manual pipeline stage.
535
551
  - **`aq trade-lock --on|--off|--auto|--status`** — manually overrides `main.py`'s sticky total-drawdown trade lock (see `development/v2_architecture.md`'s Manual Trade-Lock Override Contract). `--off` deliberately clears an otherwise-permanent lock; `--auto` returns to fully automatic behavior.
536
552
  - **`aq fetch <crypto|stock> --ticker <TICKER> --start <YYYY-MM-DD> --end <YYYY-MM-DD> [--apply]`** — fetches historical OHLCV from Yahoo Finance for a ticker that isn't in `config.json` yet, formats it into Lean's zip/CSV convention, and writes it to the right spot under `data/` (`data/crypto/coinbase/daily/<ticker>_trade.zip` or `data/equity/usa/daily/<ticker>.zip`). On `--apply`, it also appends a new asset block to `config.json`'s `phase1.universe.assets[]` — no manual editing needed. Dry run by default (no `--apply`): reports what would happen, writes nothing. Never runs `train.py` itself — once applied, run `python train.py --dataset-only` (then `python train.py` when ready) yourself to actually train on the new ticker. `crypto`/`stock` today; a `derivative` asset class is planned for V3.
@@ -706,5 +722,5 @@ architecture docs already identify:
706
722
  ---
707
723
 
708
724
  <div align="center">
709
- <sub>Built with Python · PyTorch · QuantConnect Lean · FastAPI · React · Redis · PostgreSQL · Docker</sub>
725
+ <sub>Aether Quant</sub>
710
726
  </div>
@@ -1,5 +1,5 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aether-quant
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Aether Quant convenience CLI
5
5
  Requires-Python: >=3.10
@@ -8,8 +8,10 @@ Dockerfile.retraining_worker
8
8
  Dockerfile.workers
9
9
  README.md
10
10
  aq_cli.py
11
+ config.json
11
12
  docker-compose.yml
12
13
  generate_backtest_report.py
14
+ lean.json
13
15
  main.py
14
16
  pyproject.toml
15
17
  risk_controls.py
@@ -9,8 +9,10 @@
9
9
  ".env.compose.example",
10
10
  "Dockerfile.retraining_worker",
11
11
  "train_topology.py",
12
+ "config.json",
12
13
  "train.py",
13
14
  ".env.live.example",
15
+ "lean.json",
14
16
  ".dockerignore",
15
17
  "generate_backtest_report.py",
16
18
  ".gitattributes",
@@ -1,7 +1,7 @@
1
1
  {
2
- "tag": "0.5.0",
2
+ "tag": "0.6.0",
3
3
  "distance": 0,
4
- "node": "gb230bc437a96905925c7aa521e7006da67cfb98c",
4
+ "node": "g5cffff5750320cc223918c89314db15f96f5365b",
5
5
  "dirty": false,
6
6
  "branch": "HEAD",
7
7
  "node_date": "2026-07-07"
@@ -53,3 +53,29 @@ risk engine, and before action categorization / Lean order placement.
53
53
  actions are written into the per-asset `signal_payload` for
54
54
  dashboard/Grafana visibility regardless of whether a real order is
55
55
  placed.
56
+ - **Real composite scoring (additive, config-gated).** Until now, every
57
+ priority tier here checked exactly one raw field in isolation against a
58
+ fixed threshold (confidence alone, `topology_risk` alone,
59
+ `liquidity_action` alone) — pure if/elif routing, no aggregation. New
60
+ `compute_signal_quality_score(confidence, regime_confidence, topology,
61
+ liquidity)` computes a real bounded `[0,1]` composite — a small,
62
+ hand-tuned weighted blend of raw model confidence (0.45), regime
63
+ confidence (0.20), topology peer-support (0.20, penalized when
64
+ `topology_risk` is `isolated`/`elevated`), and liquidity friction (0.15,
65
+ penalized by `participation_rate`) — mirroring `moe/gating.py`'s
66
+ `_quality_multiplier`/`_performance_score` style: real math over
67
+ already-available fields, not a trained model. `MarketAnalysisDecision`
68
+ gains `signal_quality_score`/`signal_quality_breakdown` fields that are
69
+ **always** computed and populated on every decision, regardless of any
70
+ flag — visible in `visualization/state.json` immediately for dashboard
71
+ use. It only ever changes routing when
72
+ `phase_v2.market_analyzer.use_composite_signal_score` is explicitly
73
+ `true` (default `false`) — in that case it replaces raw `confidence`
74
+ with the composite score in the `trade` gate (priority 7) and the
75
+ `simulate`-vs-`observe` split (priority 8) only. The hard safety-override
76
+ tiers (trade-lock, `risk_off` regime, elevated topology, liquidity
77
+ blocked — priorities 1-6) are **never** affected by this flag, regardless
78
+ of its value — they stay simple categorical gates on purpose, same
79
+ reasoning as the topology-elevated/isolated rules above. Default `false`
80
+ means output is byte-identical to pre-this-change behavior everywhere
81
+ the flag isn't explicitly turned on.
@@ -4,10 +4,12 @@ from .market_analyzer import (
4
4
  ACTIONS,
5
5
  MarketAnalysisDecision,
6
6
  build_market_analysis_decision,
7
+ compute_signal_quality_score,
7
8
  )
8
9
 
9
10
  __all__ = [
10
11
  "ACTIONS",
11
12
  "MarketAnalysisDecision",
12
13
  "build_market_analysis_decision",
14
+ "compute_signal_quality_score",
13
15
  ]
@@ -4,7 +4,7 @@ action."""
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
- from dataclasses import asdict, dataclass
7
+ from dataclasses import asdict, dataclass, field
8
8
 
9
9
 
10
10
  ACTIONS = ("observe", "simulate", "trade", "reduce_risk", "retrain_candidate")
@@ -21,11 +21,57 @@ class MarketAnalysisDecision:
21
21
  topology_considered: bool
22
22
  liquidity_considered: bool
23
23
  reasons: list[str]
24
+ signal_quality_score: float = 0.0
25
+ signal_quality_breakdown: dict = field(default_factory=dict)
24
26
 
25
27
  def to_dict(self) -> dict:
26
28
  return asdict(self)
27
29
 
28
30
 
31
+ def _clamp01(value: float) -> float:
32
+ return max(0.0, min(1.0, value))
33
+
34
+
35
+ def compute_signal_quality_score(
36
+ confidence: float,
37
+ regime_confidence: float,
38
+ topology: dict,
39
+ liquidity: dict,
40
+ ) -> tuple[float, dict]:
41
+ """Bounded [0,1] composite of raw model confidence, regime confidence,
42
+ topology peer-support and liquidity friction. Always computed and
43
+ exposed (signal_quality_score/signal_quality_breakdown) for dashboard
44
+ visibility; only influences routing in build_market_analysis_decision()
45
+ when use_composite_signal_score=True - see analyzer/README.md. Small
46
+ hand-tuned weights, mirroring moe/gating.py's
47
+ _quality_multiplier/_performance_score style - not a trained model."""
48
+ confidence_component = _clamp01(float(confidence))
49
+ regime_component = _clamp01(float(regime_confidence))
50
+
51
+ correlation_strength = _clamp01(float(topology.get("correlation_strength", 0.0) or 0.0))
52
+ topology_penalty = {"isolated": 0.3, "elevated": 0.6}.get(str(topology.get("topology_risk", "unknown")), 1.0)
53
+ topology_component = correlation_strength * topology_penalty if topology else regime_component
54
+
55
+ participation_rate = _clamp01(float(liquidity.get("participation_rate", 0.0) or 0.0))
56
+ liquidity_component = _clamp01(1.0 - participation_rate) if liquidity else 1.0
57
+
58
+ weights = {"confidence": 0.45, "regime": 0.20, "topology": 0.20, "liquidity": 0.15}
59
+ score = (
60
+ weights["confidence"] * confidence_component
61
+ + weights["regime"] * regime_component
62
+ + weights["topology"] * topology_component
63
+ + weights["liquidity"] * liquidity_component
64
+ )
65
+ breakdown = {
66
+ "confidence_component": confidence_component,
67
+ "regime_component": regime_component,
68
+ "topology_component": topology_component,
69
+ "liquidity_component": liquidity_component,
70
+ "weights": dict(weights),
71
+ }
72
+ return _clamp01(score), breakdown
73
+
74
+
29
75
  def build_market_analysis_decision(
30
76
  signal_name: str,
31
77
  confidence: float,
@@ -41,6 +87,7 @@ def build_market_analysis_decision(
41
87
  min_confidence_to_trade: float = 0.12,
42
88
  retrain_min_regime_confidence: float = 0.20,
43
89
  low_regime_confidence_threshold: float = 0.35,
90
+ use_composite_signal_score: bool = False,
44
91
  ) -> MarketAnalysisDecision:
45
92
  reasons: list[str] = []
46
93
  decision_source = str(gating.get("decision_source", "unknown"))
@@ -57,6 +104,17 @@ def build_market_analysis_decision(
57
104
  else:
58
105
  reasons.append("topology_absent_v2_11_pending")
59
106
 
107
+ # Always computed and exposed for dashboard visibility, regardless of
108
+ # use_composite_signal_score - see compute_signal_quality_score()'s
109
+ # docstring and analyzer/README.md. trade_metric is what priorities 7/8
110
+ # actually gate on below: the composite score when the flag is on,
111
+ # otherwise raw confidence - byte-identical to pre-flag behavior when
112
+ # use_composite_signal_score=False (the default).
113
+ signal_quality_score, signal_quality_breakdown = compute_signal_quality_score(
114
+ confidence, regime_confidence, topology, liquidity
115
+ )
116
+ trade_metric = signal_quality_score if use_composite_signal_score else confidence
117
+
60
118
  # Priority 1: reduce_risk - portfolio-level risk lock always wins.
61
119
  if trade_lock_active:
62
120
  reasons.append(trade_lock_reason or "risk_lock_active")
@@ -70,6 +128,8 @@ def build_market_analysis_decision(
70
128
  topology_considered=topology_considered,
71
129
  liquidity_considered=liquidity_considered,
72
130
  reasons=reasons,
131
+ signal_quality_score=signal_quality_score,
132
+ signal_quality_breakdown=signal_quality_breakdown,
73
133
  )
74
134
 
75
135
  # Priority 2: reduce_risk - asset-level risk regime override even
@@ -86,6 +146,8 @@ def build_market_analysis_decision(
86
146
  topology_considered=topology_considered,
87
147
  liquidity_considered=liquidity_considered,
88
148
  reasons=reasons,
149
+ signal_quality_score=signal_quality_score,
150
+ signal_quality_breakdown=signal_quality_breakdown,
89
151
  )
90
152
 
91
153
  # Priority 3: reduce_risk - elevated cross-sectional volatility pressure
@@ -106,6 +168,8 @@ def build_market_analysis_decision(
106
168
  topology_considered=topology_considered,
107
169
  liquidity_considered=liquidity_considered,
108
170
  reasons=reasons,
171
+ signal_quality_score=signal_quality_score,
172
+ signal_quality_breakdown=signal_quality_breakdown,
109
173
  )
110
174
 
111
175
  # Priority 4: retrain_candidate - zero experts contributing AND the
@@ -125,6 +189,8 @@ def build_market_analysis_decision(
125
189
  topology_considered=topology_considered,
126
190
  liquidity_considered=liquidity_considered,
127
191
  reasons=reasons,
192
+ signal_quality_score=signal_quality_score,
193
+ signal_quality_breakdown=signal_quality_breakdown,
128
194
  )
129
195
 
130
196
  # Priority 5: simulate - liquidity blocked (zero volume or DDV below
@@ -142,6 +208,8 @@ def build_market_analysis_decision(
142
208
  topology_considered=topology_considered,
143
209
  liquidity_considered=liquidity_considered,
144
210
  reasons=reasons,
211
+ signal_quality_score=signal_quality_score,
212
+ signal_quality_breakdown=signal_quality_breakdown,
145
213
  )
146
214
 
147
215
  # Priority 6: simulate - thin market; participation rate would be
@@ -158,15 +226,20 @@ def build_market_analysis_decision(
158
226
  topology_considered=topology_considered,
159
227
  liquidity_considered=liquidity_considered,
160
228
  reasons=reasons,
229
+ signal_quality_score=signal_quality_score,
230
+ signal_quality_breakdown=signal_quality_breakdown,
161
231
  )
162
232
 
163
233
  # Priority 7: trade - only for trading-eligible assets with an actionable
164
234
  # directional signal, sufficient confidence, no topology isolation, and
165
235
  # no liquidity block (redundant guard — tiers 5/6 already caught those).
236
+ # Gates on trade_metric, not raw confidence directly - see trade_metric's
237
+ # definition above (identical to confidence unless
238
+ # use_composite_signal_score=True).
166
239
  if (
167
240
  trading_eligible
168
241
  and signal_name in {"buy", "sell"}
169
- and confidence >= min_confidence_to_trade
242
+ and trade_metric >= min_confidence_to_trade
170
243
  and topology_risk != "isolated"
171
244
  and liquidity_action not in {"block", "simulate_instead"}
172
245
  ):
@@ -181,6 +254,8 @@ def build_market_analysis_decision(
181
254
  topology_considered=topology_considered,
182
255
  liquidity_considered=liquidity_considered,
183
256
  reasons=reasons,
257
+ signal_quality_score=signal_quality_score,
258
+ signal_quality_breakdown=signal_quality_breakdown,
184
259
  )
185
260
 
186
261
  # Priority 8: simulate vs observe.
@@ -201,6 +276,8 @@ def build_market_analysis_decision(
201
276
  topology_considered=topology_considered,
202
277
  liquidity_considered=liquidity_considered,
203
278
  reasons=reasons,
279
+ signal_quality_score=signal_quality_score,
280
+ signal_quality_breakdown=signal_quality_breakdown,
204
281
  )
205
282
 
206
283
  reasons.append("no_actionable_edge")
@@ -214,4 +291,6 @@ def build_market_analysis_decision(
214
291
  topology_considered=topology_considered,
215
292
  liquidity_considered=liquidity_considered,
216
293
  reasons=reasons,
294
+ signal_quality_score=signal_quality_score,
295
+ signal_quality_breakdown=signal_quality_breakdown,
217
296
  )
@@ -33,6 +33,7 @@ import sys
33
33
  import time
34
34
  import urllib.request
35
35
  import uuid
36
+ from collections.abc import Iterator
36
37
  from datetime import date
37
38
  from importlib.metadata import version as installed_version
38
39
  from pathlib import Path
@@ -42,6 +43,7 @@ from risk.manual_override import read_manual_trade_lock_override, write_manual_t
42
43
 
43
44
  ROOT_DIR = Path(__file__).resolve().parent
44
45
  CONFIG_PATH = ROOT_DIR / "config.json"
46
+ LEAN_JSON_PATH = ROOT_DIR / "lean.json"
45
47
  WEBUI_DIR = ROOT_DIR / "webui"
46
48
  README_PATH = ROOT_DIR / "README.md"
47
49
 
@@ -393,6 +395,115 @@ def cmd_trade_lock(args: argparse.Namespace) -> int:
393
395
  return 0
394
396
 
395
397
 
398
+ class ConfigPathError(Exception):
399
+ """Raised by _get_config_value/_set_config_value for a bad dotted path."""
400
+
401
+
402
+ def _get_config_value(config: dict, dotted_path: str) -> object:
403
+ node = config
404
+ walked: list[str] = []
405
+ for segment in dotted_path.split("."):
406
+ walked.append(segment)
407
+ if not isinstance(node, dict) or segment not in node:
408
+ raise ConfigPathError(f"no such config key: {'.'.join(walked)!r}")
409
+ node = node[segment]
410
+ return node
411
+
412
+
413
+ def _coerce_config_value(raw: str) -> object:
414
+ """JSON-first parsing so true/false/123/0.5/[...]/{...} all become their
415
+ real type automatically; anything that isn't valid JSON on its own
416
+ (e.g. a bare word) is kept as a plain string."""
417
+ try:
418
+ return json.loads(raw)
419
+ except json.JSONDecodeError:
420
+ return raw
421
+
422
+
423
+ def _set_config_value(config: dict, dotted_path: str, raw_value: str) -> tuple[object, object, bool]:
424
+ """Mutates `config` in place. Returns (old_value, new_value, type_changed) -
425
+ deliberately does not refuse to overwrite a list/dict: the caller wants
426
+ full read/write access to every key, not just scalars. Safety instead
427
+ comes from always reporting old -> new (and a type-change warning) to
428
+ the caller, plus the automatic config.json.bak snapshot cmd_config()
429
+ writes before every set."""
430
+ *parents, leaf = dotted_path.split(".")
431
+ node = config
432
+ for segment in parents:
433
+ if not isinstance(node, dict) or segment not in node:
434
+ raise ConfigPathError(f"no such config key: {dotted_path!r}")
435
+ node = node[segment]
436
+ if not isinstance(node, dict) or leaf not in node:
437
+ raise ConfigPathError(f"no such config key: {dotted_path!r}")
438
+ old_value = node[leaf]
439
+ new_value = _coerce_config_value(raw_value)
440
+ node[leaf] = new_value
441
+ return old_value, new_value, type(old_value) is not type(new_value)
442
+
443
+
444
+ def _iter_leaf_paths(node: object, prefix: str = "") -> Iterator[str]:
445
+ """Recursively yields every dot-joined leaf path under `node`. A "leaf"
446
+ is any non-dict value (or an empty dict) - list-valued keys show up as
447
+ one leaf, never expanded per-element."""
448
+ if isinstance(node, dict) and node:
449
+ for key, value in node.items():
450
+ yield from _iter_leaf_paths(value, f"{prefix}.{key}" if prefix else key)
451
+ else:
452
+ yield prefix
453
+
454
+
455
+ def _dispatch_json_config_command(args: argparse.Namespace, json_path: Path, command_attr: str) -> int:
456
+ """Shared dispatch for `aq config`/`aq lean` - both are the same
457
+ dump/get/set/keys tool over a single flat JSON file, just pointed at a
458
+ different path. See cmd_config()/cmd_lean()."""
459
+ data = json.loads(json_path.read_text(encoding="utf-8"))
460
+ command = getattr(args, command_attr, None)
461
+
462
+ try:
463
+ if command is None:
464
+ print(json.dumps(data, indent=2))
465
+ return 0
466
+
467
+ if command == "get":
468
+ value = _get_config_value(data, args.dotted_path)
469
+ print(value if isinstance(value, str) else json.dumps(value, indent=2))
470
+ return 0
471
+
472
+ if command == "keys":
473
+ root = _get_config_value(data, args.dotted_prefix) if args.dotted_prefix else data
474
+ for path in _iter_leaf_paths(root, args.dotted_prefix or ""):
475
+ print(path)
476
+ return 0
477
+
478
+ if command == "set":
479
+ shutil.copy2(json_path, json_path.with_suffix(".json.bak"))
480
+ old_value, new_value, type_changed = _set_config_value(data, args.dotted_path, args.value)
481
+ json_path.write_text(json.dumps(data, indent=4) + "\n", encoding="utf-8")
482
+ print(f"{args.dotted_path}: {old_value!r} -> {new_value!r}")
483
+ if type_changed:
484
+ print(
485
+ f"WARNING: type changed from {type(old_value).__name__} to {type(new_value).__name__} "
486
+ f"for {args.dotted_path}",
487
+ file=sys.stderr,
488
+ )
489
+ return 0
490
+ except ConfigPathError as error:
491
+ print(f"error: {error}", file=sys.stderr)
492
+ return 1
493
+
494
+ return 1
495
+
496
+
497
+ def cmd_config(args: argparse.Namespace) -> int:
498
+ return _dispatch_json_config_command(args, CONFIG_PATH, "config_command")
499
+
500
+
501
+ def cmd_lean(args: argparse.Namespace) -> int:
502
+ return _dispatch_json_config_command(args, LEAN_JSON_PATH, "lean_command")
503
+
504
+ return 1
505
+
506
+
396
507
  def cmd_fetch(args: argparse.Namespace) -> int:
397
508
  report = fetch_adhoc_asset(args.asset_class, args.ticker, args.start, args.end, apply=args.apply)
398
509
  label = "APPLY" if args.apply else "DRY RUN"
@@ -464,12 +575,51 @@ def build_parser() -> argparse.ArgumentParser:
464
575
  docker_build_parser = docker_subparsers.add_parser("build", help="Rebuild the aether-quant app image")
465
576
  docker_build_parser.set_defaults(func=cmd_docker_build)
466
577
 
578
+ config_parser = subparsers.add_parser("config", help="Show or edit config.json")
579
+ config_parser.set_defaults(func=cmd_config)
580
+ config_subparsers = config_parser.add_subparsers(dest="config_command")
581
+
582
+ config_get_parser = config_subparsers.add_parser("get", help="Print a config.json value")
583
+ config_get_parser.add_argument("dotted_path")
584
+
585
+ config_keys_parser = config_subparsers.add_parser("keys", help="List leaf key paths (optionally scoped to a prefix)")
586
+ config_keys_parser.add_argument("dotted_prefix", nargs="?", default=None)
587
+
588
+ config_set_parser = config_subparsers.add_parser("set", help="Set a config.json value (JSON-parsed, string fallback)")
589
+ config_set_parser.add_argument("dotted_path")
590
+ config_set_parser.add_argument("value")
591
+
592
+ lean_parser = subparsers.add_parser("lean", help="Show or edit lean.json (same shape as `aq config`)")
593
+ lean_parser.set_defaults(func=cmd_lean)
594
+ lean_subparsers = lean_parser.add_subparsers(dest="lean_command")
595
+
596
+ lean_get_parser = lean_subparsers.add_parser("get", help="Print a lean.json value")
597
+ lean_get_parser.add_argument("dotted_path")
598
+
599
+ lean_keys_parser = lean_subparsers.add_parser("keys", help="List leaf key paths (optionally scoped to a prefix)")
600
+ lean_keys_parser.add_argument("dotted_prefix", nargs="?", default=None)
601
+
602
+ lean_set_parser = lean_subparsers.add_parser("set", help="Set a lean.json value (JSON-parsed, string fallback)")
603
+ lean_set_parser.add_argument("dotted_path")
604
+ lean_set_parser.add_argument("value")
605
+
467
606
  retrain_parser = subparsers.add_parser(
468
607
  "retrain", help="Thin dispatcher to python -m retraining.orchestrator <stage> ..."
469
608
  )
470
609
  retrain_parser.add_argument(
471
610
  "stage",
472
- choices=["plan", "train", "train_topology", "validate", "backtest", "commit", "promote", "rollback", "status"],
611
+ choices=[
612
+ "plan",
613
+ "train",
614
+ "train_topology",
615
+ "train_gating",
616
+ "validate",
617
+ "backtest",
618
+ "commit",
619
+ "promote",
620
+ "rollback",
621
+ "status",
622
+ ],
473
623
  )
474
624
  retrain_parser.add_argument("retrain_args", nargs=argparse.REMAINDER, help="Passed through verbatim, e.g. --version-id <uuid>")
475
625
  retrain_parser.set_defaults(func=cmd_retrain)