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.
- aether_quant-0.0.1.dist-info/METADATA +7 -0
- aether_quant-0.0.1.dist-info/RECORD +99 -0
- aether_quant-0.0.1.dist-info/WHEEL +5 -0
- aether_quant-0.0.1.dist-info/entry_points.txt +2 -0
- aether_quant-0.0.1.dist-info/licenses/LICENSE +133 -0
- aether_quant-0.0.1.dist-info/top_level.txt +13 -0
- analyzer/README.md +69 -0
- analyzer/__init__.py +17 -0
- analyzer/market_analyzer.py +521 -0
- aq_cli.py +3633 -0
- audit/README.md +47 -0
- audit/__init__.py +38 -0
- audit/hash_chain.py +52 -0
- audit/postgres_audit.py +61 -0
- audit/postgres_worker.py +402 -0
- audit/redis_queue.py +124 -0
- audit/status_export.py +62 -0
- data_pipeline/README.md +257 -0
- data_pipeline/__init__.py +6 -0
- data_pipeline/bar_synthesis.py +97 -0
- data_pipeline/dividend_backfill.py +243 -0
- data_pipeline/factor_file_backfill.py +276 -0
- data_pipeline/fetch.py +206 -0
- data_pipeline/fred_backfill.py +435 -0
- data_pipeline/ib_backfill.py +287 -0
- data_pipeline/v2_manifest.py +75 -0
- data_pipeline/yfinance_backfill.py +501 -0
- evaluation/README.md +152 -0
- evaluation/__init__.py +73 -0
- evaluation/ablation.py +310 -0
- evaluation/benchmark_comparison.py +195 -0
- evaluation/confidence_threshold_calibration.py +157 -0
- evaluation/factor_neutralization_check.py +94 -0
- evaluation/feature_parity.py +321 -0
- evaluation/feature_reconciliation.py +286 -0
- evaluation/impact_model.py +77 -0
- evaluation/kill_switch_replay.py +186 -0
- evaluation/limit_fill_simulator.py +152 -0
- evaluation/model_predictions.py +192 -0
- evaluation/monte_carlo.py +161 -0
- evaluation/public_benchmarks.py +117 -0
- evaluation/rank_book_simulator.py +494 -0
- evaluation/rank_ic_core.py +204 -0
- evaluation/rank_signal_calibration.py +690 -0
- evaluation/rolling_ic_gate_calibration.py +190 -0
- evaluation/rolling_ic_gate_replay.py +138 -0
- execution/README.md +281 -0
- execution/__init__.py +80 -0
- execution/config_cache.py +48 -0
- execution/cost_model.py +213 -0
- execution/lean_config_render.py +131 -0
- execution/live_credentials.py +38 -0
- execution/live_credentials_io.py +52 -0
- execution/order_gate.py +388 -0
- execution/paper_readiness.py +181 -0
- execution/paper_readiness_io.py +54 -0
- execution/paper_readiness_report.py +103 -0
- execution/paper_readiness_scheduler.py +137 -0
- execution/reconciliation.py +160 -0
- execution/runtime_config_io.py +38 -0
- execution/secret_scan.py +73 -0
- features/README.md +77 -0
- features/__init__.py +137 -0
- features/alt_data_features.py +154 -0
- features/bond_features.py +275 -0
- features/cross_asset_sensitivity.py +79 -0
- features/derivatives_macro_features.py +72 -0
- features/macro_features.py +79 -0
- features/options_greeks.py +329 -0
- features/sector_map.py +53 -0
- features/technical_indicators.py +185 -0
- generate_backtest_report.py +311 -0
- generate_evaluation_report.py +712 -0
- inference/README.md +105 -0
- inference/__init__.py +36 -0
- inference/exported_model.py +929 -0
- inference/parallel_inference.py +160 -0
- inference/strategy_selector_inference.py +75 -0
- json_safety.py +130 -0
- portfolio/README.md +447 -0
- portfolio/__init__.py +41 -0
- portfolio/book_construction.py +786 -0
- portfolio/book_neutrality.py +178 -0
- portfolio/hrp_allocation.py +159 -0
- portfolio/options_arbitrage_detector.py +232 -0
- portfolio/options_assignment_risk.py +119 -0
- portfolio/options_margin_sizing.py +333 -0
- portfolio/options_strategy.py +1999 -0
- portfolio/rank_signal.py +219 -0
- portfolio/rolling_ic_gate.py +183 -0
- risk/README.md +275 -0
- risk/__init__.py +38 -0
- risk/asset_class_router.py +584 -0
- risk/forex_risk.py +158 -0
- risk/futures_risk.py +202 -0
- risk/kill_switch.py +261 -0
- risk/manual_override.py +118 -0
- risk/position_sizing.py +303 -0
- 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
|
+
|
data_pipeline/README.md
ADDED
|
@@ -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,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
|