diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ff4c8b5..5d83c41 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -64,12 +64,19 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - id: ver + run: echo "tag=$(date -u +%Y.%m.%d)-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" - uses: docker/build-push-action@v6 with: context: . push: true + build-args: | + GIT_SHA=${{ github.sha }} + BUILD_TAG=${{ steps.ver.outputs.tag }} tags: | ghcr.io/fez5587/momentum-app:latest ghcr.io/fez5587/momentum-app:${{ github.sha }} + ghcr.io/fez5587/momentum-app:${{ steps.ver.outputs.tag }} fez5587/momentum-app:latest fez5587/momentum-app:${{ github.sha }} + fez5587/momentum-app:${{ steps.ver.outputs.tag }} diff --git a/Dockerfile b/Dockerfile index 12f89cd..602f6e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,13 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends libpq5 ffmpeg curl \ && rm -rf /var/lib/apt/lists/* +# Image identity: CI passes the git sha + a date tag so the RUNNING app can +# report exactly what it is (TrueNAS custom apps show only 'custom/1.0.0'). +ARG GIT_SHA=dev +ARG BUILD_TAG=dev +ENV APP_GIT_SHA=$GIT_SHA \ + APP_BUILD_TAG=$BUILD_TAG + ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ PYTHONPATH=/app \ diff --git a/admin_api.py b/admin_api.py index a31efde..b1725e4 100644 --- a/admin_api.py +++ b/admin_api.py @@ -321,7 +321,9 @@ class Handler(BaseHTTPRequestHandler): return self._send(401, {"error": "bad or missing X-Admin-Token"}) if path == "/api/status": return self._send(200, {"containers": list_containers(), "checks": checks(), - "overrides": _safe_overrides()}) + "overrides": _safe_overrides(), + "version": os.environ.get("APP_BUILD_TAG", "dev"), + "git_sha": os.environ.get("APP_GIT_SHA", "dev")[:12]}) if path == "/api/config": eff = {k: v for k, v in sorted(os.environ.items()) if _editable(k)} secrets = {k: bool(os.environ.get(k)) for k in diff --git a/api/main.py b/api/main.py index 181b15d..b49cedd 100644 --- a/api/main.py +++ b/api/main.py @@ -878,7 +878,9 @@ class DashboardHandler(BaseHTTPRequestHandler): days = int((self._query().get("days") or ["7"])[0]) self._send_json(compute_attribution(days=days)) elif route == "/api/health": - self._send_json({"ok": True, "mode": self.state.execution_mode}) + self._send_json({"ok": True, "mode": self.state.execution_mode, + "version": os.environ.get("APP_BUILD_TAG", "dev"), + "git_sha": os.environ.get("APP_GIT_SHA", "dev")[:12]}) else: self._send_json({"error": "not found"}, 404) except Exception as exc: # noqa: BLE001 diff --git a/eod_replay.py b/eod_replay.py index 9ba9ad0..b7c1b6c 100644 --- a/eod_replay.py +++ b/eod_replay.py @@ -23,6 +23,12 @@ from dotenv import load_dotenv sys.path.insert(0, ".") load_dotenv() + +# config parity with the live loop: DB-backed knobs (config_overrides) beat +# compose env — a bare env would replay under DEFAULT exits (the 4.5x-baseline +# landmine, orderflow-shelf-verdicts 2026-07-17) +from runtime.env_overrides import load as _load_cfg_overrides # noqa: E402 +_load_cfg_overrides() os.environ.setdefault("STRATEGY_SETUPS", "opening_range_break") from alpaca_paper.client import AlpacaPaperClient diff --git a/nightly_tune.py b/nightly_tune.py index b114eb3..74635e2 100644 --- a/nightly_tune.py +++ b/nightly_tune.py @@ -26,6 +26,12 @@ from research.multi_schema import open_research_db load_dotenv() +# config parity with the live loop: DB-backed knobs (config_overrides) beat +# compose env — a bare env would replay under DEFAULT exits (the 4.5x-baseline +# landmine, orderflow-shelf-verdicts 2026-07-17) +from runtime.env_overrides import load as _load_cfg_overrides # noqa: E402 +_load_cfg_overrides() + def _write_heartbeat(outcome: str) -> None: """Record that the tuner RAN (regardless of whether it found a profitable diff --git a/research/experiments/cli.py b/research/experiments/cli.py index 1427627..b396a5f 100644 --- a/research/experiments/cli.py +++ b/research/experiments/cli.py @@ -11,6 +11,12 @@ from __future__ import annotations import os import sys +# config parity with the live loop: DB-backed knobs (config_overrides) beat +# compose env — a bare env would replay under DEFAULT exits (the 4.5x-baseline +# landmine, orderflow-shelf-verdicts 2026-07-17) +from runtime.env_overrides import load as _load_cfg_overrides # noqa: E402 +_load_cfg_overrides() + def _store(): from storage.event_store import EventStore diff --git a/research/ingestion/market_context.py b/research/ingestion/market_context.py index 4b732ec..65548c4 100644 --- a/research/ingestion/market_context.py +++ b/research/ingestion/market_context.py @@ -26,6 +26,12 @@ from research.ingestion.market_data import ( # noqa: E402 upsert_daily_bars, upsert_minute_bars) from research.multi_schema import open_research_db # noqa: E402 +# config parity with the live loop: DB-backed knobs (config_overrides) beat +# compose env — a bare env would replay under DEFAULT exits (the 4.5x-baseline +# landmine, orderflow-shelf-verdicts 2026-07-17) +from runtime.env_overrides import load as _load_cfg_overrides # noqa: E402 +_load_cfg_overrides() + INDEX_SYMBOLS = ("SPY", "QQQ") diff --git a/research/premarket_eval.py b/research/premarket_eval.py index 63d4ecd..188bf14 100644 --- a/research/premarket_eval.py +++ b/research/premarket_eval.py @@ -18,6 +18,12 @@ from dotenv import load_dotenv from research import labeler from research.multi_schema import open_research_db +# config parity with the live loop: DB-backed knobs (config_overrides) beat +# compose env — a bare env would replay under DEFAULT exits (the 4.5x-baseline +# landmine, orderflow-shelf-verdicts 2026-07-17) +from runtime.env_overrides import load as _load_cfg_overrides # noqa: E402 +_load_cfg_overrides() + load_dotenv(os.path.join(os.path.dirname(__file__), "..", ".env")) HIST = os.path.join(os.path.dirname(__file__), "..", "data", "premarket_eval_history.tsv") diff --git a/research/regime.py b/research/regime.py index f560c77..665022b 100644 --- a/research/regime.py +++ b/research/regime.py @@ -37,6 +37,12 @@ from datetime import date, datetime, timedelta, timezone from pathlib import Path from statistics import mean, median, pstdev +# config parity with the live loop: DB-backed knobs (config_overrides) beat +# compose env — a bare env would replay under DEFAULT exits (the 4.5x-baseline +# landmine, orderflow-shelf-verdicts 2026-07-17) +from runtime.env_overrides import load as _load_cfg_overrides # noqa: E402 +_load_cfg_overrides() + _SCHEMA_SQL = Path(__file__).resolve().parent.parent / "storage" / "regime_schema.sql" # Pre-open / prior-close-lagged features only. See module docstring + plan 011 diff --git a/tests/unit/test_env_overrides_db.py b/tests/unit/test_env_overrides_db.py index 3aa8257..ae7e587 100644 --- a/tests/unit/test_env_overrides_db.py +++ b/tests/unit/test_env_overrides_db.py @@ -29,6 +29,7 @@ def test_read_missing_table_is_empty(con): def test_load_db_applies_onto_environ(con, monkeypatch): + monkeypatch.setenv("ENV_OVERRIDES_DB", "1") # force on (harness may disable) env_overrides.write_db_overrides({"TRADING_TEST_KNOB_XYZ": "42"}, con=con) monkeypatch.setattr(env_overrides, "_db_con", lambda: con) monkeypatch.setenv("TRADING_TEST_KNOB_XYZ", "1") # compose value loses