aether-quant 0.0.1__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.
Files changed (99) hide show
  1. aether_quant-0.0.1.dist-info/METADATA +7 -0
  2. aether_quant-0.0.1.dist-info/RECORD +99 -0
  3. aether_quant-0.0.1.dist-info/WHEEL +5 -0
  4. aether_quant-0.0.1.dist-info/entry_points.txt +2 -0
  5. aether_quant-0.0.1.dist-info/licenses/LICENSE +133 -0
  6. aether_quant-0.0.1.dist-info/top_level.txt +13 -0
  7. analyzer/README.md +69 -0
  8. analyzer/__init__.py +17 -0
  9. analyzer/market_analyzer.py +521 -0
  10. aq_cli.py +3633 -0
  11. audit/README.md +47 -0
  12. audit/__init__.py +38 -0
  13. audit/hash_chain.py +52 -0
  14. audit/postgres_audit.py +61 -0
  15. audit/postgres_worker.py +402 -0
  16. audit/redis_queue.py +124 -0
  17. audit/status_export.py +62 -0
  18. data_pipeline/README.md +257 -0
  19. data_pipeline/__init__.py +6 -0
  20. data_pipeline/bar_synthesis.py +97 -0
  21. data_pipeline/dividend_backfill.py +243 -0
  22. data_pipeline/factor_file_backfill.py +276 -0
  23. data_pipeline/fetch.py +206 -0
  24. data_pipeline/fred_backfill.py +435 -0
  25. data_pipeline/ib_backfill.py +287 -0
  26. data_pipeline/v2_manifest.py +75 -0
  27. data_pipeline/yfinance_backfill.py +501 -0
  28. evaluation/README.md +152 -0
  29. evaluation/__init__.py +73 -0
  30. evaluation/ablation.py +310 -0
  31. evaluation/benchmark_comparison.py +195 -0
  32. evaluation/confidence_threshold_calibration.py +157 -0
  33. evaluation/factor_neutralization_check.py +94 -0
  34. evaluation/feature_parity.py +321 -0
  35. evaluation/feature_reconciliation.py +286 -0
  36. evaluation/impact_model.py +77 -0
  37. evaluation/kill_switch_replay.py +186 -0
  38. evaluation/limit_fill_simulator.py +152 -0
  39. evaluation/model_predictions.py +192 -0
  40. evaluation/monte_carlo.py +161 -0
  41. evaluation/public_benchmarks.py +117 -0
  42. evaluation/rank_book_simulator.py +494 -0
  43. evaluation/rank_ic_core.py +204 -0
  44. evaluation/rank_signal_calibration.py +690 -0
  45. evaluation/rolling_ic_gate_calibration.py +190 -0
  46. evaluation/rolling_ic_gate_replay.py +138 -0
  47. execution/README.md +281 -0
  48. execution/__init__.py +80 -0
  49. execution/config_cache.py +48 -0
  50. execution/cost_model.py +213 -0
  51. execution/lean_config_render.py +131 -0
  52. execution/live_credentials.py +38 -0
  53. execution/live_credentials_io.py +52 -0
  54. execution/order_gate.py +388 -0
  55. execution/paper_readiness.py +181 -0
  56. execution/paper_readiness_io.py +54 -0
  57. execution/paper_readiness_report.py +103 -0
  58. execution/paper_readiness_scheduler.py +137 -0
  59. execution/reconciliation.py +160 -0
  60. execution/runtime_config_io.py +38 -0
  61. execution/secret_scan.py +73 -0
  62. features/README.md +77 -0
  63. features/__init__.py +137 -0
  64. features/alt_data_features.py +154 -0
  65. features/bond_features.py +275 -0
  66. features/cross_asset_sensitivity.py +79 -0
  67. features/derivatives_macro_features.py +72 -0
  68. features/macro_features.py +79 -0
  69. features/options_greeks.py +329 -0
  70. features/sector_map.py +53 -0
  71. features/technical_indicators.py +185 -0
  72. generate_backtest_report.py +311 -0
  73. generate_evaluation_report.py +712 -0
  74. inference/README.md +105 -0
  75. inference/__init__.py +36 -0
  76. inference/exported_model.py +929 -0
  77. inference/parallel_inference.py +160 -0
  78. inference/strategy_selector_inference.py +75 -0
  79. json_safety.py +130 -0
  80. portfolio/README.md +447 -0
  81. portfolio/__init__.py +41 -0
  82. portfolio/book_construction.py +786 -0
  83. portfolio/book_neutrality.py +178 -0
  84. portfolio/hrp_allocation.py +159 -0
  85. portfolio/options_arbitrage_detector.py +232 -0
  86. portfolio/options_assignment_risk.py +119 -0
  87. portfolio/options_margin_sizing.py +333 -0
  88. portfolio/options_strategy.py +1999 -0
  89. portfolio/rank_signal.py +219 -0
  90. portfolio/rolling_ic_gate.py +183 -0
  91. risk/README.md +275 -0
  92. risk/__init__.py +38 -0
  93. risk/asset_class_router.py +584 -0
  94. risk/forex_risk.py +158 -0
  95. risk/futures_risk.py +202 -0
  96. risk/kill_switch.py +261 -0
  97. risk/manual_override.py +118 -0
  98. risk/position_sizing.py +303 -0
  99. risk/rl_sizing.py +189 -0
audit/redis_queue.py ADDED
@@ -0,0 +1,124 @@
1
+ """Redis Stream publisher for the audit log (development/Problems.md #42).
2
+
3
+ Mirrors experience/redis_queue.py exactly - same fire-and-forget, never-raises
4
+ publish contract, so audit logging can be called from main.py's hot per-bar
5
+ order-placement path (and from execution/'s credential-load/live-mode-transition
6
+ call sites) without ever risking a real trade or a live process on a Postgres
7
+ round-trip. All Redis failures are caught and logged; nothing downstream of
8
+ this queue is ever blocking.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ import os
15
+ import uuid
16
+ from datetime import datetime, timezone
17
+ from typing import Any
18
+
19
+ from json_safety import dumps_json_safe
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # The three event categories development/Problems.md #42 named as needing a
24
+ # tamper-evident trail before real capital. Kept as a closed set (not
25
+ # free-form strings) so a typo at a call site fails loudly in tests rather
26
+ # than silently creating a new, unqueryable event_type.
27
+ ORDER_PLACEMENT = "order_placement"
28
+ CREDENTIAL_LOAD = "credential_load"
29
+ LIVE_MODE_TRANSITION = "live_mode_transition"
30
+ EVENT_TYPES = (ORDER_PLACEMENT, CREDENTIAL_LOAD, LIVE_MODE_TRANSITION)
31
+
32
+
33
+ def build_audit_event(event_type: str, payload: dict, actor: str = "system") -> dict[str, Any]:
34
+ """Construct a standardised audit event dict. Pure - no I/O.
35
+
36
+ `payload` should never contain a raw secret value - callers logging a
37
+ credential-load event pass field NAMES that were populated (see
38
+ execution/lean_config_render.py's own "never the values" convention),
39
+ never the credential itself. `actor` defaults to "system" (an automated
40
+ call site, e.g. main.py's order placement) - execution/'s CLI-driven
41
+ call sites (aq render-lean-config) pass "cli" instead."""
42
+ if event_type not in EVENT_TYPES:
43
+ raise ValueError(f"Unknown audit event_type: {event_type!r} (expected one of {EVENT_TYPES})")
44
+ return {
45
+ "event_id": str(uuid.uuid4()),
46
+ "event_type": event_type,
47
+ "created_at": datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
48
+ "actor": actor,
49
+ "payload": payload,
50
+ }
51
+
52
+
53
+ class AuditQueue:
54
+ """Fire-and-forget Redis Stream publisher for audit events.
55
+
56
+ Identical shape/contract to experience.redis_queue.ExperienceQueue -
57
+ all failures (connection errors, XADD errors, missing package) are
58
+ caught and logged at WARNING level. push() never raises.
59
+
60
+ Parameters
61
+ ----------
62
+ enabled : kill switch - if False, push() is always a no-op
63
+ redis_url : connection URL; overridden by AETHER_REDIS_URL env var
64
+ stream_name : Redis key for XADD (default: "aether:audit")
65
+ maxlen : approximate cap on stream length (MAXLEN ~)
66
+ _client : pre-built Redis client for test injection
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ enabled: bool = True,
72
+ redis_url: str = "redis://localhost:6380/0",
73
+ stream_name: str = "aether:audit",
74
+ maxlen: int = 500_000,
75
+ _client=None,
76
+ ) -> None:
77
+ self.enabled = enabled
78
+ self.stream_name = stream_name
79
+ self.maxlen = maxlen
80
+ self._client = None
81
+
82
+ if not enabled:
83
+ return
84
+
85
+ if _client is not None:
86
+ self._client = _client
87
+ return
88
+
89
+ url = os.environ.get("AETHER_REDIS_URL", redis_url) or "redis://localhost:6380/0"
90
+ try:
91
+ import redis as redis_lib # deferred — not always installed in Lean environments
92
+
93
+ client = redis_lib.from_url(url, socket_connect_timeout=2, socket_timeout=2)
94
+ client.ping()
95
+ self._client = client
96
+ logger.info(
97
+ "AuditQueue connected to %s (stream=%s, maxlen=%d)",
98
+ url,
99
+ stream_name,
100
+ maxlen,
101
+ )
102
+ except Exception as exc:
103
+ logger.warning(
104
+ "AuditQueue: Redis unavailable at %s — audit events will be skipped. (%s)",
105
+ url,
106
+ exc,
107
+ )
108
+
109
+ def push(self, event: dict) -> bool:
110
+ """Publish one event dict to the Redis Stream. Returns True on
111
+ success, False on any failure. Never raises."""
112
+ if not self.enabled or self._client is None:
113
+ return False
114
+ try:
115
+ self._client.xadd(
116
+ self.stream_name,
117
+ {"payload": dumps_json_safe(event)},
118
+ maxlen=self.maxlen,
119
+ approximate=True,
120
+ )
121
+ return True
122
+ except Exception as exc:
123
+ logger.warning("AuditQueue.push failed: %s", exc)
124
+ return False
audit/status_export.py ADDED
@@ -0,0 +1,62 @@
1
+ """Dashboard/monitoring JSON export for the audit log (development/Problems.md
2
+ #42). Same reason as retraining/status_export.py: main.py never connects to
3
+ Postgres, only Redis, so monitoring/api_server.py can't query audit_log
4
+ directly - this module is the sole writer of
5
+ visualization/grafana/audit_log.json, refreshed by audit/postgres_worker.py
6
+ after every batch it persists (real-time-ish, not a periodic cron export,
7
+ since a stale audit trail defeats its own purpose).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from datetime import date, datetime, timezone
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from json_safety import atomic_write_json
17
+ from .hash_chain import verify_chain
18
+ from .postgres_audit import fetch_all_events_ordered, fetch_recent_events
19
+
20
+ ROOT_DIR = Path(__file__).resolve().parent.parent
21
+ DEFAULT_STATUS_PATH = ROOT_DIR / "visualization" / "grafana" / "audit_log.json"
22
+
23
+ # Dashboard snapshot only ever shows the most recent N entries (unlike
24
+ # `aq audit-log`'s CLI, which can page further back) - the webui panel is
25
+ # "what's happened recently," not a full audit browser.
26
+ _DASHBOARD_ENTRY_LIMIT = 50
27
+
28
+
29
+ def _json_safe(value: Any) -> Any:
30
+ if isinstance(value, (datetime, date)):
31
+ return value.isoformat()
32
+ if isinstance(value, dict):
33
+ return {key: _json_safe(item) for key, item in value.items()}
34
+ if isinstance(value, (list, tuple)):
35
+ return [_json_safe(item) for item in value]
36
+ return value
37
+
38
+
39
+ def build_audit_status_view(conn) -> dict:
40
+ """{generated_at, recent_events, chain_valid, total_entries}. Chain
41
+ verification runs over the FULL table (fetch_all_events_ordered), not
42
+ just the dashboard's recent-N slice - a tampered OLD entry must still
43
+ surface as chain_valid=False even if it's scrolled off the recent list."""
44
+ recent = fetch_recent_events(conn, limit=_DASHBOARD_ENTRY_LIMIT)
45
+ all_rows = fetch_all_events_ordered(conn)
46
+ chain_valid, broken_index = verify_chain(all_rows)
47
+
48
+ return {
49
+ "generated_at": datetime.now(timezone.utc).isoformat(),
50
+ "total_entries": len(all_rows),
51
+ "chain_valid": chain_valid,
52
+ "chain_broken_at_event_id": all_rows[broken_index]["event_id"] if broken_index is not None else None,
53
+ "recent_events": [_json_safe(event) for event in recent],
54
+ }
55
+
56
+
57
+ def write_status_file(status: dict, path: Path = DEFAULT_STATUS_PATH) -> None:
58
+ # V5.4.7 (Problems.md #120): atomic + finite-safe - the API reader
59
+ # previously raced this write and 500'd on torn JSON.
60
+ atomic_write_json(path, status, indent=2)
61
+
62
+
@@ -0,0 +1,257 @@
1
+ # data_pipeline
2
+
3
+ Owns the Lean-data pipeline contract and the data-ingestion tooling around
4
+ it.
5
+
6
+ The core rule stays simple and important: training and backtesting use the local Lean `data/` folder. This package describes that contract for the downstream modules (MoE experts, regime detection, topology modeling, dynamic risk, the dashboards) so they can depend on a stable manifest.
7
+
8
+ It does not replace `train.py`; it wraps and documents the existing dataset pipeline.
9
+
10
+ ## Modules
11
+
12
+ - `v2_manifest.py` — the V2 metadata contract for the Lean-data training
13
+ pipeline (the "stable manifest" the rule above promises).
14
+ - `fetch.py` — `aq fetch` (see below).
15
+ - `yfinance_backfill.py` — Yahoo Finance historical backfill (see below).
16
+ - `fred_backfill.py` — Treasury yield/credit-spread backfill (see below).
17
+ - `ib_backfill.py` — Interactive Brokers futures/options backfill (see below).
18
+ - `factor_file_backfill.py` — Lean-format split/dividend **factor-file**
19
+ backfill (the price-adjustment factors Lean reads alongside the price
20
+ zips; the missing-factor-file class of data-quality issue behind
21
+ `development/Problems.md` #91/#97/#99).
22
+ - `dividend_backfill.py` — real ex-dividend history via `yfinance`, with
23
+ next-date projection from historical cadence; feeds
24
+ `portfolio/options_assignment_risk.py`'s early-assignment scoring.
25
+ - `bar_synthesis.py` (V5.3.10) — pure functions extracted from `main.py`
26
+ so they are unit-testable without a Lean runtime:
27
+ `midpoint_bar_from_quote_bar()` (synthesizes a bar from forex/crypto
28
+ quote bars) and `pad_sequence_history()` (pads short history for the
29
+ sequence encoder), plus the adaptive sell-threshold helper. Parity-tested
30
+ in `tests/test_bar_synthesis_parity.py`.
31
+
32
+ ## Yahoo Finance historical data backfill
33
+
34
+ `yfinance_backfill.py` is a deliberate, narrow exception to the rule above:
35
+ it is the first thing in this package that reaches outside the local Lean
36
+ `data/` folder, to fill gaps in thin series (initially `ETHUSD`/`LTCUSD`,
37
+ which have only a few scattered days of real Coinbase minute data — see
38
+ `train.py::ensure_derived_crypto_daily_series()` and this repo's Changelog).
39
+ It is a **manual, offline maintenance script** — never invoked from
40
+ `train.py`, `main.py`, or any Docker worker; no network calls happen during
41
+ training or a backtest.
42
+
43
+ - Per-asset opt-in via a `"backfill"` sub-block in `config.json`'s
44
+ `phase1.universe.assets[]` entries (`source`, `symbol`, `backfill_from`,
45
+ `backfill_to`). Assets without it are skipped entirely.
46
+ - Fetched Yahoo rows are converted to the exact Lean zip format
47
+ (`train.py::ensure_derived_crypto_daily_series()`'s row/member-name
48
+ convention) and merged into the existing zip — **existing real Lean rows
49
+ always win on any overlapping date**; Yahoo data only fills genuine gaps.
50
+ - **Dry run by default.** Writing zip files requires an explicit `--apply`
51
+ flag. Regardless of `--apply`, this script **never** edits `config.json`'s
52
+ `available_from`/`available_to` — it only prints the suggested new values,
53
+ because `train.py::build_asset_quality()` only counts rows inside those
54
+ configured windows, so a human must deliberately widen them for the extra
55
+ history to actually affect training/backtesting.
56
+
57
+ Usage: `python -m data_pipeline.yfinance_backfill [--tickers ETHUSD LTCUSD] [--apply]`.
58
+ `yfinance` is a dev-only dependency (`requirements/requirements-dev.txt`),
59
+ never in `requirements.txt` (the one requirements file the consolidated
60
+ `aether-quant-engine` Docker image installs).
61
+
62
+ ## Ad-hoc ticker fetch (`aq fetch`)
63
+
64
+ `fetch.py` is a narrower sibling to `yfinance_backfill.py` above, with a
65
+ different scope and a deliberately different `config.json` policy:
66
+
67
+ | | `yfinance_backfill.py` | `fetch.py` (backs `aq fetch`) |
68
+ |---|---|---|
69
+ | Scope | Fills gaps in **already-configured** assets | Fetches **any** ticker, including ones with no `config.json` entry at all |
70
+ | Date range | Auto-detected gap vs. the asset's `"backfill"` block | Explicit `--start`/`--end` |
71
+ | `config.json` | **Never** edited — only prints suggested values | **Writes** a new asset block on `--apply`, for a ticker that isn't already configured |
72
+
73
+ The differing `config.json` policy is deliberate, not an inconsistency:
74
+ `yfinance_backfill.py`'s rule exists to stop it from silently widening an
75
+ *existing*, already-trained asset's date range without a human decision.
76
+ `fetch.py` exists specifically to *add a brand-new ticker* to the universe
77
+ on purpose — auto-writing `config.json` there is the intended effect, not
78
+ an accidental side effect. If a ticker fetched via `aq fetch` already has a
79
+ `config.json` entry, `fetch.py` leaves it untouched and points at
80
+ `yfinance_backfill.py` instead, since extending an existing asset's range
81
+ is that script's job, not this one's.
82
+
83
+ Reuses `yfinance_backfill.py`'s pure functions (`fetch_yahoo_ohlcv`,
84
+ `scale_for_lean`, `write_lean_zip`) unchanged — no duplicated logic. Same
85
+ dry-run-by-default/`--apply` safety convention. Never runs `train.py`
86
+ itself; see the root [`README.md`](../README.md#cli-reference) for the
87
+ full `aq fetch` usage.
88
+
89
+ ### Two data-provenance contracts, by design (`aq fetch` tickers are pre-adjusted)
90
+
91
+ Every ticker fetched via `fetch_adhoc_asset()` (the bond ETF sleeve added in
92
+ Phase 1 of the 5/10 -> 9/10 roadmap is the first real-universe example) goes
93
+ through `fetch_yahoo_ohlcv()`'s `auto_adjust=True` — Yahoo applies its own
94
+ split/dividend adjustment **before** the data ever reaches a Lean zip. This
95
+ is a second, deliberate data-provenance contract living alongside the
96
+ original one: the original ~15 equities (AAPL, SPY, ...) use **raw** Lean
97
+ prices, adjusted separately at train time via a checked-in
98
+ `data/equity/usa/factor_files/<ticker>.csv`
99
+ (`train.py::apply_split_adjustments()`/`load_factor_file()`). An
100
+ `aq fetch`-added ticker deliberately has **no** factor file — adding one
101
+ would double-adjust already-adjusted prices — and `load_factor_file()`'s
102
+ existing "return `None` when missing" behavior is the correct, intended
103
+ no-op for this path, not an accidental gap (see
104
+ `tests/test_train_pipeline.py::test_apply_split_adjustments_noop_is_the_intended_contract_for_aq_fetch_tickers`).
105
+ This matters more for bond ETFs than most equities, since they distribute
106
+ income frequently (often monthly) — an un-adjusted bond ETF price series
107
+ would show a fake "return" on every distribution date.
108
+
109
+ `fetch_adhoc_asset()` also does **not** write a Lean
110
+ `data/equity/usa/map_files/<ticker>.csv` — that file is Lean's own
111
+ local-disk security-resolution convention (a two-row identity map,
112
+ `<start_date>,<ticker>,Q` / `20501231,<ticker>,Q`, mirroring e.g.
113
+ `aapl.csv`) and every currently-active equity ticker in this universe
114
+ already has one (bundled with QuantConnect's sample data, since no prior
115
+ `aq fetch`-added ticker had ever been wired into an active universe before
116
+ Phase 1). Since this repo's Lean backtests are run manually by the user
117
+ (see the Runbook), rather than block on an automated `lean backtest` check
118
+ this repo doesn't currently have Docker access to run, map files were
119
+ created proactively for the bond ETF sleeve, following the exact identity
120
+ format above — any future `aq fetch`-added ticker destined for the real
121
+ universe should get one the same way until/unless a real Lean run confirms
122
+ it's unnecessary.
123
+
124
+ ## Real Treasury yield/credit-spread backfill (`fred_backfill.py`)
125
+
126
+ A third sibling, alongside `yfinance_backfill.py`/`fetch.py` above, backing
127
+ `features/bond_features.py`'s real-data yield-curve/credit-spread features
128
+ (distinct from `features/macro_features.py`'s price-momentum proxies).
129
+ Fetches FRED's public graph CSV endpoint — **no API key required**, stdlib
130
+ `urllib.request` only (no new runtime dependency, unlike
131
+ `yfinance_backfill.py`'s deferred `import yfinance`). Same
132
+ dry-run-by-default/`--apply` safety convention; writes a local cache
133
+ (`data/reference/fred_series/*.csv`, never committed as real market data
134
+ the way Lean zips are) rather than editing `config.json` at all — nothing
135
+ in this codebase's asset universe schema needs to change for bond features
136
+ to exist, since they're cross-asset macro signals broadcast to every asset,
137
+ not a new configured ticker.
138
+
139
+ `load_cached_fred_series()` is the read-side: loaded once at
140
+ `train.py`/`main.py` startup (never fetched live mid-backtest — Lean
141
+ backtests are date-bounded), `{}` if the cache was never populated (fresh
142
+ clone), in which case every bond feature neutral-defaults to `0.0` — the
143
+ same "missing reference -> 0.0" convention `macro_features.py` already
144
+ established, never a crash.
145
+
146
+ V5.4.7 (#123): cache writes MERGE by date — a narrow `--start/--end`
147
+ refetch no longer destroys observations outside its window (new rows win
148
+ their own dates). Same round: Yahoo NaN-frame rows are filtered before any
149
+ zip write, and `aq fetch --apply`'s config.json rewrite is atomic.
150
+
151
+ Usage: `python -m data_pipeline.fred_backfill [--series treasury_10yr ...] [--apply]`.
152
+
153
+ ### Alt-data extension (Phase 4.12, `development/Problems.md` #71)
154
+
155
+ The same fetcher, generic series-ID passthrough, and cache convention
156
+ above now also back `features/alt_data_features.py`: `VIXCLS` (CBOE VIX),
157
+ `VXVCLS` (3-month VIX), and `NFCI` (Chicago Fed financial conditions) —
158
+ `DEFAULT_ALT_DATA_REFERENCE_SERIES`. `--group {bond,alt,all}` selects which
159
+ reference-series set to fetch (`reference_series(config, group="all")`,
160
+ `alt_data_reference_series(config)`); `aq backfill fred --group alt
161
+ --apply` fetches only the new three.
162
+
163
+ **Publication lag is the one thing that matters here.** VIX/VXV are
164
+ same-day CBOE closes (lag `0`), but NFCI is Friday-dated and not released
165
+ until the following Wednesday — `ALT_DATA_PUBLICATION_LAG_DAYS` records
166
+ each series' lag in days, and `series_value_asof(rows, current_date,
167
+ publication_lag_days)`/`series_change_asof(rows, current_date,
168
+ publication_lag_days, periods_back)` are the lag-aware lookups both
169
+ `train.py::build_alt_data_features_by_date()` and
170
+ `main.py::_build_alt_data_payload()` use — deliberately **not** the bare
171
+ `_fred_series_asof()` bisect the bond features use, since that helper has
172
+ no lag parameter and would silently look ahead on a lagged series.
173
+
174
+ Candidate series were screened before adding (`BAMLH0A0HYM2` dropped for
175
+ insufficient free-endpoint history, `STLFSI4` rejected as near-collinear
176
+ with `NFCI`) — see `development/Changelog.md` for the screening details.
177
+
178
+ ## Interactive Brokers historical backfill (`ib_backfill.py`) — futures/options only
179
+
180
+ The futures/options sibling to `fetch.py`, but a fundamentally different
181
+ data source: `ib_insync` (a **dev-only** dependency,
182
+ `requirements/requirements-dev.txt`, never in `requirements.txt` — this
183
+ module is importable with `ib_insync` absent as long as IB stays
184
+ disabled, same deferred-import convention as `yfinance_backfill.py`).
185
+
186
+ This is deliberately a **separate integration** from Lean's own native
187
+ live/paper Interactive Brokers brokerage (`lean.json`'s
188
+ `ib-account`/`ib-user-name`/`ib-password`/`ib-trading-mode` fields, and
189
+ `environments.live-interactive` — untouched by this module, see root
190
+ `README.md`'s `aq ib` section for the full explanation of why there are
191
+ two IB integrations, not one). Lean's `environments.backtesting` is
192
+ hardcoded to `FileSystemDataFeed`/`SubscriptionDataReaderHistoryProvider`
193
+ — it never talks to IB regardless of `lean.json`'s contents — so
194
+ historical futures/options bars still need this separate, offline
195
+ data-prep step to land as local Lean-format zip files before any backtest
196
+ can use them.
197
+
198
+ - **"IB ready" requires both**: `config.json`'s `phase_v2.ib.enabled ==
199
+ true` (Aether's own app-level feature flag) AND `lean.json`'s
200
+ `ib-account`/`ib-user-name` non-empty (Lean's own credential fields,
201
+ read-only from this module's perspective). `phase_v2.ib.host`/`port`/
202
+ `client_id` are TWS/IB Gateway **socket API connection settings only**
203
+ (not credentials — your Gateway session already handles login), a
204
+ second, independent API client alongside whatever Lean's own brokerage
205
+ uses when live-deployed (standard IB API pattern — Gateway supports
206
+ multiple simultaneous API clients).
207
+ - Every public function raises `IBNotConfiguredError` — never a raw
208
+ traceback — when IB isn't ready, so `aq fetch futures`/`aq fetch
209
+ options` fail cleanly with a message pointing at `aq ib status`.
210
+ - Plugs into `fetch.py::ASSET_CLASS_CONFIG`'s `"futures"`/`"options"`
211
+ entries the same way `yfinance_backfill.py`'s functions plug into
212
+ `"crypto"`/`"stock"` — `aq_cli.py::cmd_fetch()` builds a closure binding
213
+ the connected IB client + contract spec/expiry/strike/right before
214
+ calling the same `fetch_adhoc_asset()` every asset class shares.
215
+
216
+ Usage: `aq fetch futures --ticker ES --start ... --end ... --expiry ... [--apply]`,
217
+ `aq fetch options --ticker SPY --start ... --end ... --expiry ... --strike ... --right call [--apply]`
218
+ — see root `README.md`'s CLI Reference.
219
+
220
+ ### Building a real training-time derivatives dataset (manual, per-contract)
221
+
222
+ `train.py::build_derivatives_macro_features_by_date()` computes real
223
+ futures term structure / options put-call-ratio / IV-skew features from
224
+ whatever future/option assets are configured in `config.json` — but IB's
225
+ historical API is per-contract and rate-limited, so there is no single
226
+ bulk-fetch command for this. Building a useful training window means
227
+ repeating `aq fetch futures`/`aq fetch options` once per contract:
228
+
229
+ - **Futures term structure** needs at least two same-root contracts tagged
230
+ with the same `--family-ticker` and *different* `--contract-month`
231
+ (`YYYYMM`), e.g.:
232
+ ```text
233
+ aq fetch futures --ticker ES_FRONT --family-ticker ES --contract-month 202603 --start ... --end ... --expiry ... --apply
234
+ aq fetch futures --ticker ES_NEXT --family-ticker ES --contract-month 202606 --start ... --end ... --expiry ... --apply
235
+ ```
236
+ Without `--contract-month`, `aq fetch futures` fetches Lean's default
237
+ *continuous* contract — fine for price/return features, but a single
238
+ continuous series can't produce a term-structure slope; you need two
239
+ distinct dated contracts for that.
240
+ - **Options sentiment (put/call ratio, IV skew)** needs option assets
241
+ tagged with `--family-ticker <underlying>` (matching
242
+ `phase1.features.derivatives_reference_tickers.options_sentiment` in
243
+ `config.json`, default `"SPY"`) plus their own `--strike`/`--expiry`/
244
+ `--right`, e.g.:
245
+ ```text
246
+ aq fetch options --ticker SPY_500C --family-ticker SPY --strike 500 --expiry 2026-08-21 --right call --start ... --end ... --apply
247
+ aq fetch options --ticker SPY_490P --family-ticker SPY --strike 490 --expiry 2026-08-21 --right put --start ... --end ... --apply
248
+ ```
249
+ - Any family/underlying with too few tagged contracts (or missing
250
+ `strike`/`expiry`/`right` metadata) resolves to the neutral default
251
+ (0.0) for that feature, silently — never a crash. This is the honest
252
+ "no data configured" case, not a bug; run `aq assets status` to see
253
+ what's currently configured.
254
+ - After fetching, run `python train.py --dataset-only` (then `python
255
+ train.py` when ready) to actually train on the new data — `aq fetch`
256
+ never trains anything itself.
257
+
@@ -0,0 +1,6 @@
1
+ """Lean-data pipeline helpers for Aether Quant V2."""
2
+
3
+ from .v2_manifest import build_v2_pipeline_manifest
4
+
5
+ __all__ = ["build_v2_pipeline_manifest"]
6
+
@@ -0,0 +1,97 @@
1
+ """V5.3.10 - pure functions extracted from main.py so the live-path logic
2
+ they implement becomes unit-testable outside Lean's runtime (established
3
+ pattern: evaluate_non_model_exit(), compute_incremental_order_quantity(),
4
+ etc.). Each function is a VERBATIM copy of the corresponding main.py
5
+ method body - see each function's docstring for its origin line."""
6
+
7
+ from __future__ import annotations
8
+
9
+ from types import SimpleNamespace
10
+
11
+
12
+ def midpoint_bar_from_quote_bar(quote_bar):
13
+ """V4.6 - forex quote-bar (bid/ask) to trade-bar-shaped midpoint OHLC
14
+ (main.py::_midpoint_bar_from_quote_bar verbatim). Volume is always
15
+ 0.0 - forex quote data carries no trade-volume concept. Returns None
16
+ (never raises) when no quote bar exists or it's missing a bid/ask side."""
17
+ if quote_bar is None or quote_bar.Bid is None or quote_bar.Ask is None:
18
+ return None
19
+ return SimpleNamespace(
20
+ open=(float(quote_bar.Bid.Open) + float(quote_bar.Ask.Open)) / 2.0,
21
+ high=(float(quote_bar.Bid.High) + float(quote_bar.Ask.High)) / 2.0,
22
+ low=(float(quote_bar.Bid.Low) + float(quote_bar.Ask.Low)) / 2.0,
23
+ close=(float(quote_bar.Bid.Close) + float(quote_bar.Ask.Close)) / 2.0,
24
+ volume=0.0,
25
+ )
26
+
27
+
28
+ def pad_sequence_history(history: list[list[float]], window_size: int) -> list[list[float]]:
29
+ """Left-pads a rolling per-symbol feature-history buffer with zero
30
+ vectors up to window_size (main.py::_pad_sequence_history verbatim).
31
+ Pure, no side effects. An empty history returns `window_size` all-zero
32
+ vectors of the model's width when inferable, else raises a clear
33
+ ValueError instead of IndexError (V5.4.7 #123)."""
34
+ if not history:
35
+ raise ValueError(
36
+ "pad_sequence_history: empty history - feature width cannot be "
37
+ "inferred; callers guard this before calling (main.py's own "
38
+ "truthy filters), so reaching here means a caller contract broke."
39
+ )
40
+ input_width = len(history[0])
41
+ padding_needed = window_size - len(history)
42
+ return [[0.0] * input_width for _ in range(max(0, padding_needed))] + list(history)
43
+
44
+
45
+ def adaptive_sell_threshold(
46
+ *,
47
+ sell_threshold: float,
48
+ history: list[float] | None,
49
+ band_enabled: bool,
50
+ min_observations: int,
51
+ band_percentile: float,
52
+ ) -> float:
53
+ """Pure lookup + percentile over accumulated probability_up history
54
+ (main.py::_effective_sell_threshold verbatim). Never adapts ABOVE the
55
+ static threshold - the band only makes selling easier."""
56
+ if not band_enabled:
57
+ return sell_threshold
58
+ if history is None or len(history) < min_observations:
59
+ return sell_threshold
60
+ sorted_history = sorted(history)
61
+ index = max(0, min(len(sorted_history) - 1, int(band_percentile * len(sorted_history))))
62
+ return min(sell_threshold, sorted_history[index])
63
+
64
+
65
+ def derive_signal(
66
+ probability_up: float,
67
+ *,
68
+ decision_threshold: float,
69
+ buy_threshold: float,
70
+ effective_sell_threshold: float,
71
+ max_position_weight: float,
72
+ record_observation=None,
73
+ symbol_key: str | None = None,
74
+ ) -> tuple[str, float, float]:
75
+ """Signal classification extracted from main.py::_derive_signal()
76
+ (verbatim arithmetic). `record_observation(symbol_key, probability_up)`
77
+ is an optional callback so callers can preserve the observation-recording
78
+ side effect without this function needing state."""
79
+ confidence = abs(probability_up - decision_threshold) / max(1.0 - decision_threshold, decision_threshold)
80
+ confidence = max(0.0, min(confidence, 1.0))
81
+
82
+ def _record():
83
+ if record_observation is not None and symbol_key is not None:
84
+ record_observation(symbol_key, probability_up)
85
+
86
+ if probability_up >= buy_threshold:
87
+ target_weight = min(max_position_weight, 0.10 + 0.15 * confidence)
88
+ _record()
89
+ return "buy", confidence, target_weight
90
+
91
+ if probability_up <= effective_sell_threshold:
92
+ target_weight = -min(max_position_weight, 0.10 + 0.15 * confidence)
93
+ _record()
94
+ return "sell", confidence, target_weight
95
+
96
+ _record()
97
+ return "hold", confidence, 0.0