diff --git a/README.md b/README.md index 635c279..d70c8b5 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ should start from the Moomoo sections below. Current high-level surfaces: -- Web dashboard: `http://localhost:3000` -- API: `http://127.0.0.1:8000` +- Local web dashboard: `http://localhost:42130` +- Local API: `http://127.0.0.1:42180` - Public live snapshot smoke endpoint: `GET /api/spx/0dte/snapshot/latest` - Moomoo dashboard collector: `pnpm collector:moomoo-snapshot` - Moomoo ML research recorder: `pnpm collector:moomoo-research-record` @@ -40,6 +40,19 @@ Run: .venv/bin/python -m pip install -e "apps/api[dev]" pnpm test +### Local Port Assignments + +Use these uncommon ports for local development and test runs so GammaScope does not collide with the usual local web/API/database services: + +| Service | Local URL or host port | Source | +| --- | --- | --- | +| Web app | `http://127.0.0.1:42130` | `pnpm dev:web` | +| FastAPI backend | `http://127.0.0.1:42180` | `pnpm dev:api` | +| Postgres | `127.0.0.1:42432` -> container `5432` | `docker compose up -d postgres` | +| Redis | `127.0.0.1:42379` -> container `6379` | `docker compose up -d redis` | + +Keep the deployed AMH/Nginx server mappings on the documented production ports, currently web `3000` and API `8000`. The uncommon ports above are the repo defaults for local testing only. + ### Verification pnpm install @@ -55,10 +68,10 @@ Run: Run local services: docker compose up -d + pnpm dev:api pnpm dev:web - .venv/bin/python -m uvicorn gammascope_api.main:app --reload --app-dir apps/api -Open the local dashboard at `http://localhost:3000`. The dashboard reads the +Open the local dashboard at `http://localhost:42130`. The dashboard reads the stable SPX 0DTE analytics contract. With live collector state available it shows live mode; otherwise it falls back to seeded replay data. @@ -150,7 +163,7 @@ For now this keeps the latest collector health, contracts, underlying ticks, and Replay capture now persists replay-ready analytics snapshots to local Postgres when a valid live collector snapshot is available. The API uses: - GAMMASCOPE_DATABASE_URL=postgresql://gammascope:gammascope@127.0.0.1:5432/gammascope + GAMMASCOPE_DATABASE_URL=postgresql://gammascope:gammascope@127.0.0.1:42432/gammascope GAMMASCOPE_REPLAY_CAPTURE_INTERVAL_SECONDS=5 GAMMASCOPE_REPLAY_RETENTION_DAYS=20 GAMMASCOPE_SAVED_VIEW_RETENTION_DAYS=90 @@ -164,15 +177,15 @@ With the API running, publish the mock collector cycle into that ingestion endpo Then inspect the live-mode analytics snapshot assembled from the ingested collector state: - curl -s http://127.0.0.1:8000/api/spx/0dte/snapshot/latest | python -m json.tool + curl -s http://127.0.0.1:42180/api/spx/0dte/snapshot/latest | python -m json.tool To test the dashboard against local API state, run the API, publish the mock cycle, then start the web app with the API base URL: pnpm dev:api pnpm collector:publish-mock -- --spot 5200.25 --expiry 2026-04-24 --strikes 5190,5200,5210 - GAMMASCOPE_API_BASE_URL=http://127.0.0.1:8000 pnpm dev:web + GAMMASCOPE_API_BASE_URL=http://127.0.0.1:42180 pnpm dev:web -Open `http://localhost:3000`. After the mock publish populates API state, the dashboard should show Live mode; if the API is unavailable, the web app falls back to the seeded replay snapshot. After page load, the dashboard connects to `ws://127.0.0.1:8000/ws/spx/0dte` for live snapshot updates and falls back to once-per-second polling if the WebSocket is unavailable. Set `NEXT_PUBLIC_GAMMASCOPE_WS_URL` when the WebSocket endpoint is not on the default local API host. +Open `http://localhost:42130`. After the mock publish populates API state, the dashboard should show Live mode; if the API is unavailable, the web app falls back to the seeded replay snapshot. After page load, the dashboard connects to `ws://127.0.0.1:42180/ws/spx/0dte` for live snapshot updates and falls back to once-per-second polling if the WebSocket is unavailable. Set `NEXT_PUBLIC_GAMMASCOPE_WS_URL` when the WebSocket endpoint is not on the default local API host. The dashboard also includes lightweight saved views for local testing. Saved views are validated against the shared contract, proxied through the Next.js app, and persisted in Postgres using `GAMMASCOPE_DATABASE_URL` when available. If the Postgres-backed repository is unavailable at runtime, the FastAPI route falls back to in-memory saved views so local dashboard flows keep working. @@ -190,14 +203,14 @@ Set private mode when the API may be reachable by non-admin users: In private mode, public viewing remains open. Live snapshots, live status, scenarios, live WebSocket updates, replay, heatmap, and experimental analytics do not require an admin token: - curl -s http://127.0.0.1:8000/api/spx/0dte/replay/sessions | python -m json.tool - curl -s "http://127.0.0.1:8000/api/spx/0dte/replay/snapshot?session_id=seed-spx-2026-04-23" | python -m json.tool - curl -s http://127.0.0.1:8000/api/spx/0dte/snapshot/latest | python -m json.tool + curl -s http://127.0.0.1:42180/api/spx/0dte/replay/sessions | python -m json.tool + curl -s "http://127.0.0.1:42180/api/spx/0dte/replay/snapshot?session_id=seed-spx-2026-04-23" | python -m json.tool + curl -s http://127.0.0.1:42180/api/spx/0dte/snapshot/latest | python -m json.tool Collector ingestion, raw collector state, replay imports, and maintenance/admin operations require the admin token: curl -s -H "X-GammaScope-Admin-Token: local-admin-token" \ - http://127.0.0.1:8000/api/spx/0dte/collector/state | python -m json.tool + http://127.0.0.1:42180/api/spx/0dte/collector/state | python -m json.tool Saved-view public requests list only `owner_scope: "public_demo"`; creating or listing admin scoped views requires the admin token. If `GAMMASCOPE_ADMIN_TOKEN` is unset or blank, private admin operations return `403`. @@ -273,13 +286,13 @@ Use `--market-data-type 3` to force delayed streaming during market hours, or `- After publishing, inspect captured replay sessions: - curl -s http://127.0.0.1:8000/api/spx/0dte/replay/sessions | python -m json.tool + curl -s http://127.0.0.1:42180/api/spx/0dte/replay/sessions | python -m json.tool Use the captured `session_id` to replay the persisted IBKR snapshot: - curl -s "http://127.0.0.1:8000/api/spx/0dte/replay/snapshot?session_id=" | python -m json.tool + curl -s "http://127.0.0.1:42180/api/spx/0dte/replay/snapshot?session_id=" | python -m json.tool -Then open `http://localhost:3000`, use the replay controls, and pick the captured session. The seeded replay session remains available as a fallback demo. +Then open `http://localhost:42130`, use the replay controls, and pick the captured session. The seeded replay session remains available as a fallback demo. ### Local Moomoo 0DTE Snapshot @@ -411,7 +424,7 @@ and still safely before open during standard-time months. ### SPX 0DTE Exposure Heatmap -The latest-ladder heatmap page is available at `http://localhost:3000/heatmap` when the web app is running. +The latest-ladder heatmap page is available at `http://localhost:42130/heatmap` when the web app is running. The backend API is: @@ -440,14 +453,14 @@ The `.gammascope/` directory is ignored by git and must stay local. Do not commi For local maintenance testing, run a default-safe dry run of persisted replay and saved-view retention cleanup: - curl -s -X POST "http://127.0.0.1:8000/api/admin/retention/cleanup?dry_run=true" | python -m json.tool + curl -s -X POST "http://127.0.0.1:42180/api/admin/retention/cleanup?dry_run=true" | python -m json.tool To execute destructive cleanup explicitly: GAMMASCOPE_ADMIN_TOKEN=local-admin-token pnpm dev:api curl -s -X POST \ -H "X-GammaScope-Admin-Token: local-admin-token" \ - "http://127.0.0.1:8000/api/admin/retention/cleanup?dry_run=false" | python -m json.tool + "http://127.0.0.1:42180/api/admin/retention/cleanup?dry_run=false" | python -m json.tool If `GAMMASCOPE_ADMIN_TOKEN` is unset or blank, destructive cleanup is disabled and returns `403`. Cleanup only targets Postgres-persisted replay snapshots/sessions and saved views. The seeded replay fixture remains untouched. diff --git a/apps/api/gammascope_api/ingestion/latest_state_cache.py b/apps/api/gammascope_api/ingestion/latest_state_cache.py index 8c87944..40ed31e 100644 --- a/apps/api/gammascope_api/ingestion/latest_state_cache.py +++ b/apps/api/gammascope_api/ingestion/latest_state_cache.py @@ -9,7 +9,7 @@ from gammascope_api.ingestion.collector_state import CollectorState, collector_state -DEFAULT_REDIS_URL = "redis://127.0.0.1:6379/0" +DEFAULT_REDIS_URL = "redis://127.0.0.1:42379/0" DEFAULT_REDIS_TIMEOUT_SECONDS = 0.25 LATEST_STATE_KEY = "gammascope:collector:latest_state" diff --git a/apps/api/gammascope_api/replay/dependencies.py b/apps/api/gammascope_api/replay/dependencies.py index a029706..7267880 100644 --- a/apps/api/gammascope_api/replay/dependencies.py +++ b/apps/api/gammascope_api/replay/dependencies.py @@ -8,7 +8,7 @@ from gammascope_api.replay.import_repository import PostgresReplayImportRepository, ReplayImportRepository from gammascope_api.replay.repository import PostgresReplayRepository, ReplayRepository -DEFAULT_DATABASE_URL = "postgresql://gammascope:gammascope@127.0.0.1:5432/gammascope" +DEFAULT_DATABASE_URL = "postgresql://gammascope:gammascope@127.0.0.1:42432/gammascope" DEFAULT_CAPTURE_INTERVAL_SECONDS = 5 _repository_override: ReplayRepository | None = None diff --git a/apps/api/tests/test_replay_import_real_files.py b/apps/api/tests/test_replay_import_real_files.py index a7dfa3e..f26ea54 100644 --- a/apps/api/tests/test_replay_import_real_files.py +++ b/apps/api/tests/test_replay_import_real_files.py @@ -13,7 +13,7 @@ from gammascope_api.replay.import_repository import PostgresReplayImportRepository -TEST_DATABASE_URL = "postgresql://gammascope:gammascope@127.0.0.1:5432/gammascope" +TEST_DATABASE_URL = "postgresql://gammascope:gammascope@127.0.0.1:42432/gammascope" def test_real_april_22_baseline_validates_when_present( diff --git a/apps/api/tests/test_replay_import_repository.py b/apps/api/tests/test_replay_import_repository.py index 14bf75d..46ff2a6 100644 --- a/apps/api/tests/test_replay_import_repository.py +++ b/apps/api/tests/test_replay_import_repository.py @@ -14,7 +14,7 @@ from replay_parquet_fixtures import write_replay_parquet_pair -TEST_DATABASE_URL = "postgresql://gammascope:gammascope@127.0.0.1:5432/gammascope" +TEST_DATABASE_URL = "postgresql://gammascope:gammascope@127.0.0.1:42432/gammascope" THIS_FILE = Path(__file__) diff --git a/apps/api/tests/test_replay_persistence.py b/apps/api/tests/test_replay_persistence.py index 71e3a6d..8552460 100644 --- a/apps/api/tests/test_replay_persistence.py +++ b/apps/api/tests/test_replay_persistence.py @@ -25,7 +25,7 @@ from gammascope_api.replay.repository import PostgresReplayRepository -TEST_DATABASE_URL = "postgresql://gammascope:gammascope@127.0.0.1:5432/gammascope" +TEST_DATABASE_URL = "postgresql://gammascope:gammascope@127.0.0.1:42432/gammascope" @pytest.fixture() diff --git a/apps/api/tests/test_saved_views_persistence.py b/apps/api/tests/test_saved_views_persistence.py index 4b07e52..d0c2c42 100644 --- a/apps/api/tests/test_saved_views_persistence.py +++ b/apps/api/tests/test_saved_views_persistence.py @@ -7,7 +7,7 @@ from gammascope_api.saved_views.repository import PostgresSavedViewRepository -TEST_DATABASE_URL = "postgresql://gammascope:gammascope@127.0.0.1:5432/gammascope" +TEST_DATABASE_URL = "postgresql://gammascope:gammascope@127.0.0.1:42432/gammascope" @pytest.fixture() diff --git a/apps/web/app/api/spx/0dte/experimental/latest/route.ts b/apps/web/app/api/spx/0dte/experimental/latest/route.ts index 3b784b3..8a6d5c9 100644 --- a/apps/web/app/api/spx/0dte/experimental/latest/route.ts +++ b/apps/web/app/api/spx/0dte/experimental/latest/route.ts @@ -2,7 +2,7 @@ import { NextResponse } from "next/server"; import seedExperimentalAnalytics from "../../../../../../../../packages/contracts/fixtures/experimental-analytics.seed.json"; import { verifyAdminRequest } from "../../../../../../lib/adminSession"; -const DEFAULT_API_BASE_URL = "http://127.0.0.1:8000"; +const DEFAULT_API_BASE_URL = "http://127.0.0.1:42180"; const EXPERIMENTAL_LATEST_PATH = "/api/spx/0dte/experimental/latest"; const ADMIN_TOKEN_HEADER = "X-GammaScope-Admin-Token"; diff --git a/apps/web/app/api/spx/0dte/experimental/replay/snapshot/route.ts b/apps/web/app/api/spx/0dte/experimental/replay/snapshot/route.ts index a0541be..cfcf34f 100644 --- a/apps/web/app/api/spx/0dte/experimental/replay/snapshot/route.ts +++ b/apps/web/app/api/spx/0dte/experimental/replay/snapshot/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server"; -const DEFAULT_API_BASE_URL = "http://127.0.0.1:8000"; +const DEFAULT_API_BASE_URL = "http://127.0.0.1:42180"; const EXPERIMENTAL_REPLAY_PATH = "/api/spx/0dte/experimental/replay/snapshot"; function experimentalReplayUrl(apiBaseUrl: string, requestUrl: string): string { diff --git a/apps/web/app/api/spx/0dte/heatmap/latest/route.ts b/apps/web/app/api/spx/0dte/heatmap/latest/route.ts index d5a1bed..ef7b840 100644 --- a/apps/web/app/api/spx/0dte/heatmap/latest/route.ts +++ b/apps/web/app/api/spx/0dte/heatmap/latest/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import { verifyAdminRequest } from "../../../../../../lib/adminSession"; -const DEFAULT_API_BASE_URL = "http://127.0.0.1:8000"; +const DEFAULT_API_BASE_URL = "http://127.0.0.1:42180"; const HEATMAP_PATH = "/api/spx/0dte/heatmap/latest"; const ADMIN_TOKEN_HEADER = "X-GammaScope-Admin-Token"; diff --git a/apps/web/app/api/spx/0dte/replay/sessions/[sessionId]/timestamps/route.ts b/apps/web/app/api/spx/0dte/replay/sessions/[sessionId]/timestamps/route.ts index 4be5eb9..5c6c7e6 100644 --- a/apps/web/app/api/spx/0dte/replay/sessions/[sessionId]/timestamps/route.ts +++ b/apps/web/app/api/spx/0dte/replay/sessions/[sessionId]/timestamps/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import { isReplayTimestampResponse } from "../../../../../../../../lib/clientReplaySource"; -const DEFAULT_API_BASE_URL = "http://127.0.0.1:8000"; +const DEFAULT_API_BASE_URL = "http://127.0.0.1:42180"; const REPLAY_SESSIONS_PATH = "/api/spx/0dte/replay/sessions"; interface ReplayTimestampsRouteContext { diff --git a/apps/web/app/api/spx/0dte/replay/sessions/route.ts b/apps/web/app/api/spx/0dte/replay/sessions/route.ts index cf69e2b..32bb84d 100644 --- a/apps/web/app/api/spx/0dte/replay/sessions/route.ts +++ b/apps/web/app/api/spx/0dte/replay/sessions/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import { isReplaySessionArray } from "../../../../../../lib/clientReplaySource"; -const DEFAULT_API_BASE_URL = "http://127.0.0.1:8000"; +const DEFAULT_API_BASE_URL = "http://127.0.0.1:42180"; const REPLAY_SESSIONS_PATH = "/api/spx/0dte/replay/sessions"; function replaySessionsUrl(apiBaseUrl: string): string { diff --git a/apps/web/app/api/spx/0dte/replay/snapshot/route.ts b/apps/web/app/api/spx/0dte/replay/snapshot/route.ts index 1f11f13..b15cea4 100644 --- a/apps/web/app/api/spx/0dte/replay/snapshot/route.ts +++ b/apps/web/app/api/spx/0dte/replay/snapshot/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import { isAnalyticsSnapshot } from "../../../../../../lib/snapshotSource"; -const DEFAULT_API_BASE_URL = "http://127.0.0.1:8000"; +const DEFAULT_API_BASE_URL = "http://127.0.0.1:42180"; const REPLAY_SNAPSHOT_PATH = "/api/spx/0dte/replay/snapshot"; function replaySnapshotUrl(apiBaseUrl: string, requestUrl: string): string { diff --git a/apps/web/app/api/spx/0dte/scenario/route.ts b/apps/web/app/api/spx/0dte/scenario/route.ts index 6bb1bec..748ac83 100644 --- a/apps/web/app/api/spx/0dte/scenario/route.ts +++ b/apps/web/app/api/spx/0dte/scenario/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import { isAnalyticsSnapshot } from "../../../../../lib/snapshotSource"; -const DEFAULT_API_BASE_URL = "http://127.0.0.1:8000"; +const DEFAULT_API_BASE_URL = "http://127.0.0.1:42180"; const SCENARIO_PATH = "/api/spx/0dte/scenario"; function scenarioUrl(apiBaseUrl: string): string { diff --git a/apps/web/app/api/spx/0dte/snapshot/latest/route.ts b/apps/web/app/api/spx/0dte/snapshot/latest/route.ts index 038c4c5..985192a 100644 --- a/apps/web/app/api/spx/0dte/snapshot/latest/route.ts +++ b/apps/web/app/api/spx/0dte/snapshot/latest/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import { verifyAdminRequest } from "../../../../../../lib/adminSession"; -const DEFAULT_API_BASE_URL = "http://127.0.0.1:8000"; +const DEFAULT_API_BASE_URL = "http://127.0.0.1:42180"; const SNAPSHOT_PATH = "/api/spx/0dte/snapshot/latest"; const ADMIN_TOKEN_HEADER = "X-GammaScope-Admin-Token"; diff --git a/apps/web/app/api/spx/0dte/status/route.ts b/apps/web/app/api/spx/0dte/status/route.ts index 94c0af3..3132830 100644 --- a/apps/web/app/api/spx/0dte/status/route.ts +++ b/apps/web/app/api/spx/0dte/status/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import { isCollectorHealth } from "../../../../../lib/clientCollectorStatusSource"; -const DEFAULT_API_BASE_URL = "http://127.0.0.1:8000"; +const DEFAULT_API_BASE_URL = "http://127.0.0.1:42180"; const COLLECTOR_STATUS_PATH = "/api/spx/0dte/status"; function collectorStatusUrl(apiBaseUrl: string): string { diff --git a/apps/web/app/api/views/route.ts b/apps/web/app/api/views/route.ts index da2a559..c482124 100644 --- a/apps/web/app/api/views/route.ts +++ b/apps/web/app/api/views/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import { isSavedViewArray, type SavedView } from "../../../lib/clientSavedViewsSource"; -const DEFAULT_API_BASE_URL = "http://127.0.0.1:8000"; +const DEFAULT_API_BASE_URL = "http://127.0.0.1:42180"; const VIEWS_PATH = "/api/views"; function viewsUrl(apiBaseUrl: string): string { diff --git a/apps/web/lib/replayImportProxy.ts b/apps/web/lib/replayImportProxy.ts index bddf273..5af0044 100644 --- a/apps/web/lib/replayImportProxy.ts +++ b/apps/web/lib/replayImportProxy.ts @@ -2,7 +2,7 @@ import { NextResponse } from "next/server"; import { verifyAdminRequest } from "./adminSession"; import { isReplayImportResult } from "./replayImportSource"; -const DEFAULT_API_BASE_URL = "http://127.0.0.1:8000"; +const DEFAULT_API_BASE_URL = "http://127.0.0.1:42180"; const DEFAULT_REPLAY_IMPORT_MAX_BYTES = 100 * 1024 * 1024; const REPLAY_IMPORT_MULTIPART_OVERHEAD_BYTES = 16 * 1024; diff --git a/apps/web/lib/replayStream.ts b/apps/web/lib/replayStream.ts index c77d081..9bdcc4f 100644 --- a/apps/web/lib/replayStream.ts +++ b/apps/web/lib/replayStream.ts @@ -2,7 +2,7 @@ import type { AnalyticsSnapshot } from "./contracts"; import { isAnalyticsSnapshot } from "./snapshotSource"; const REPLAY_WS_PATH = "/ws/spx/0dte/replay"; -const DEFAULT_API_BASE_URL = "http://127.0.0.1:8000"; +const DEFAULT_API_BASE_URL = "http://127.0.0.1:42180"; type WebSocketLike = { onclose: (() => void) | null; diff --git a/apps/web/lib/serverBackendFetch.ts b/apps/web/lib/serverBackendFetch.ts index 9c347dc..b5f7990 100644 --- a/apps/web/lib/serverBackendFetch.ts +++ b/apps/web/lib/serverBackendFetch.ts @@ -1,6 +1,6 @@ import { verifyAdminRequest } from "./adminSession"; -export const DEFAULT_API_BASE_URL = "http://127.0.0.1:8000"; +export const DEFAULT_API_BASE_URL = "http://127.0.0.1:42180"; export const ADMIN_TOKEN_HEADER = "X-GammaScope-Admin-Token"; export function backendApiUrl( diff --git a/apps/web/lib/snapshotStream.ts b/apps/web/lib/snapshotStream.ts index 86ba144..e7fcb19 100644 --- a/apps/web/lib/snapshotStream.ts +++ b/apps/web/lib/snapshotStream.ts @@ -2,7 +2,7 @@ import type { AnalyticsSnapshot } from "./contracts"; import { isAnalyticsSnapshot } from "./snapshotSource"; const SNAPSHOT_WS_PATH = "/ws/spx/0dte"; -const DEFAULT_API_BASE_URL = "http://127.0.0.1:8000"; +const DEFAULT_API_BASE_URL = "http://127.0.0.1:42180"; type WebSocketLike = { onclose: (() => void) | null; diff --git a/apps/web/tests/experimentalFlowRoute.test.ts b/apps/web/tests/experimentalFlowRoute.test.ts index 5a1c8c7..0cb694b 100644 --- a/apps/web/tests/experimentalFlowRoute.test.ts +++ b/apps/web/tests/experimentalFlowRoute.test.ts @@ -64,7 +64,7 @@ describe("GET /api/spx/0dte/experimental-flow/latest", () => { const { GET } = await import("../app/api/spx/0dte/experimental-flow/latest/route"); await GET(new Request("http://localhost/api/spx/0dte/experimental-flow/latest")); - expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:8000/api/spx/0dte/experimental-flow/latest", { + expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:42180/api/spx/0dte/experimental-flow/latest", { cache: "no-store", headers: { Accept: "application/json" @@ -86,7 +86,7 @@ describe("GET /api/spx/0dte/experimental-flow/latest", () => { } })); - expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:8000/api/spx/0dte/experimental-flow/latest", { + expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:42180/api/spx/0dte/experimental-flow/latest", { cache: "no-store", headers: { Accept: "application/json", @@ -103,7 +103,7 @@ describe("GET /api/spx/0dte/experimental-flow/latest", () => { const { GET } = await import("../app/api/spx/0dte/experimental-flow/latest/route"); await GET(new Request("http://localhost/api/spx/0dte/experimental-flow/latest")); - expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:8000/api/spx/0dte/experimental-flow/latest", { + expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:42180/api/spx/0dte/experimental-flow/latest", { cache: "no-store", headers: { Accept: "application/json" @@ -162,7 +162,7 @@ describe("GET /api/spx/0dte/experimental-flow/replay", () => { expect(response.headers.get("Content-Type")).toBe("text/plain"); expect(response.headers.get("Cache-Control")).toBe("no-store"); expect(fetcher).toHaveBeenCalledWith( - "http://127.0.0.1:8000/api/spx/0dte/experimental-flow/replay?session_id=session%2Fa&horizon_minutes=30&at=2026-04-23T15%3A40%3A00Z&source_snapshot_id=snapshot-a", + "http://127.0.0.1:42180/api/spx/0dte/experimental-flow/replay?session_id=session%2Fa&horizon_minutes=30&at=2026-04-23T15%3A40%3A00Z&source_snapshot_id=snapshot-a", { cache: "no-store", headers: { @@ -182,7 +182,7 @@ describe("GET /api/spx/0dte/experimental-flow/replay", () => { )); expect(fetcher).toHaveBeenCalledWith( - "http://127.0.0.1:8000/api/spx/0dte/experimental-flow/replay?session_id=session-a&horizon_minutes=15", + "http://127.0.0.1:42180/api/spx/0dte/experimental-flow/replay?session_id=session-a&horizon_minutes=15", { cache: "no-store", headers: { diff --git a/apps/web/tests/experimentalRoute.test.ts b/apps/web/tests/experimentalRoute.test.ts index ba8fb77..578f9a6 100644 --- a/apps/web/tests/experimentalRoute.test.ts +++ b/apps/web/tests/experimentalRoute.test.ts @@ -64,7 +64,7 @@ describe("GET /api/spx/0dte/experimental/latest", () => { const { GET } = await import("../app/api/spx/0dte/experimental/latest/route"); await GET(new Request("http://localhost/api/spx/0dte/experimental/latest")); - expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:8000/api/spx/0dte/experimental/latest", { + expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:42180/api/spx/0dte/experimental/latest", { cache: "no-store", headers: { Accept: "application/json" @@ -86,7 +86,7 @@ describe("GET /api/spx/0dte/experimental/latest", () => { } })); - expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:8000/api/spx/0dte/experimental/latest", { + expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:42180/api/spx/0dte/experimental/latest", { cache: "no-store", headers: { Accept: "application/json", @@ -103,7 +103,7 @@ describe("GET /api/spx/0dte/experimental/latest", () => { const { GET } = await import("../app/api/spx/0dte/experimental/latest/route"); await GET(new Request("http://localhost/api/spx/0dte/experimental/latest")); - expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:8000/api/spx/0dte/experimental/latest", { + expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:42180/api/spx/0dte/experimental/latest", { cache: "no-store", headers: { Accept: "application/json" @@ -162,7 +162,7 @@ describe("GET /api/spx/0dte/experimental/replay/snapshot", () => { expect(response.headers.get("Content-Type")).toBe("text/plain"); expect(response.headers.get("Cache-Control")).toBe("no-store"); expect(fetcher).toHaveBeenCalledWith( - "http://127.0.0.1:8000/api/spx/0dte/experimental/replay/snapshot?session_id=session%2Fa&at=2026-04-23T15%3A40%3A00Z&source_snapshot_id=snapshot-a", + "http://127.0.0.1:42180/api/spx/0dte/experimental/replay/snapshot?session_id=session%2Fa&at=2026-04-23T15%3A40%3A00Z&source_snapshot_id=snapshot-a", { cache: "no-store", headers: { @@ -180,7 +180,7 @@ describe("GET /api/spx/0dte/experimental/replay/snapshot", () => { await GET(new Request("http://localhost/api/spx/0dte/experimental/replay/snapshot?session_id=session-a")); expect(fetcher).toHaveBeenCalledWith( - "http://127.0.0.1:8000/api/spx/0dte/experimental/replay/snapshot?session_id=session-a", + "http://127.0.0.1:42180/api/spx/0dte/experimental/replay/snapshot?session_id=session-a", { cache: "no-store", headers: { diff --git a/apps/web/tests/heatmapRoute.test.ts b/apps/web/tests/heatmapRoute.test.ts index 36aaec8..9304fad 100644 --- a/apps/web/tests/heatmapRoute.test.ts +++ b/apps/web/tests/heatmapRoute.test.ts @@ -63,7 +63,7 @@ describe("GET /api/spx/0dte/heatmap/latest", () => { const { GET } = await import("../app/api/spx/0dte/heatmap/latest/route"); await GET(new Request("http://localhost/api/spx/0dte/heatmap/latest?metric=gex")); - expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:8000/api/spx/0dte/heatmap/latest?metric=gex", { + expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:42180/api/spx/0dte/heatmap/latest?metric=gex", { cache: "no-store", headers: { Accept: "application/json" @@ -85,7 +85,7 @@ describe("GET /api/spx/0dte/heatmap/latest", () => { } })); - expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:8000/api/spx/0dte/heatmap/latest?metric=gex", { + expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:42180/api/spx/0dte/heatmap/latest?metric=gex", { cache: "no-store", headers: { Accept: "application/json", @@ -102,7 +102,7 @@ describe("GET /api/spx/0dte/heatmap/latest", () => { const { GET } = await import("../app/api/spx/0dte/heatmap/latest/route"); await GET(new Request("http://localhost/api/spx/0dte/heatmap/latest?metric=gex")); - expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:8000/api/spx/0dte/heatmap/latest?metric=gex", { + expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:42180/api/spx/0dte/heatmap/latest?metric=gex", { cache: "no-store", headers: { Accept: "application/json" diff --git a/apps/web/tests/replayImportRoute.test.ts b/apps/web/tests/replayImportRoute.test.ts index 53aa25a..d225ebb 100644 --- a/apps/web/tests/replayImportRoute.test.ts +++ b/apps/web/tests/replayImportRoute.test.ts @@ -267,7 +267,7 @@ describe("replay import proxy routes", () => { }); expect(response.status).toBe(200); - expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:8000/api/replay/imports/import-test-1", { + expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:42180/api/replay/imports/import-test-1", { method: "GET", cache: "no-store", headers: { diff --git a/apps/web/tests/replayRoute.test.ts b/apps/web/tests/replayRoute.test.ts index fb260e8..38bc94b 100644 --- a/apps/web/tests/replayRoute.test.ts +++ b/apps/web/tests/replayRoute.test.ts @@ -51,7 +51,7 @@ describe("GET /api/spx/0dte/replay/sessions", () => { await expect(response.json()).resolves.toEqual(sessions); expect(response.headers.get("Cache-Control")).toBe("no-store"); - expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:8000/api/spx/0dte/replay/sessions", { + expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:42180/api/spx/0dte/replay/sessions", { cache: "no-store", headers: { Accept: "application/json" @@ -111,7 +111,7 @@ describe("GET /api/spx/0dte/replay/snapshot", () => { await expect(response.json()).resolves.toEqual(JSON.parse(JSON.stringify(snapshot))); expect(response.headers.get("Cache-Control")).toBe("no-store"); expect(fetcher).toHaveBeenCalledWith( - "http://127.0.0.1:8000/api/spx/0dte/replay/snapshot?session_id=route+session%2F1&at=2026-04-24T15%3A00%3A00Z", + "http://127.0.0.1:42180/api/spx/0dte/replay/snapshot?session_id=route+session%2F1&at=2026-04-24T15%3A00%3A00Z", { cache: "no-store", headers: { @@ -131,7 +131,7 @@ describe("GET /api/spx/0dte/replay/snapshot", () => { )); expect(fetcher).toHaveBeenCalledWith( - "http://127.0.0.1:8000/api/spx/0dte/replay/snapshot?session_id=route-session&at=2026-04-24T15%3A00%3A00Z&source_snapshot_id=snapshot-a", + "http://127.0.0.1:42180/api/spx/0dte/replay/snapshot?session_id=route-session&at=2026-04-24T15%3A00%3A00Z&source_snapshot_id=snapshot-a", { cache: "no-store", headers: { @@ -148,7 +148,7 @@ describe("GET /api/spx/0dte/replay/snapshot", () => { const { GET } = await import("../app/api/spx/0dte/replay/snapshot/route"); await GET(new Request("http://localhost/api/spx/0dte/replay/snapshot?session_id=route-replay-session")); - expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:8000/api/spx/0dte/replay/snapshot?session_id=route-replay-session", { + expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:42180/api/spx/0dte/replay/snapshot?session_id=route-replay-session", { cache: "no-store", headers: { Accept: "application/json" @@ -213,7 +213,7 @@ describe("GET /api/spx/0dte/replay/sessions/[sessionId]/timestamps", () => { await expect(response.json()).resolves.toEqual(timestampResponse); expect(response.headers.get("Cache-Control")).toBe("no-store"); expect(fetcher).toHaveBeenCalledWith( - "http://127.0.0.1:8000/api/spx/0dte/replay/sessions/import%20session%2F1/timestamps", + "http://127.0.0.1:42180/api/spx/0dte/replay/sessions/import%20session%2F1/timestamps", { cache: "no-store", headers: { diff --git a/apps/web/tests/replayStream.test.ts b/apps/web/tests/replayStream.test.ts index 16cd83a..69dd6a4 100644 --- a/apps/web/tests/replayStream.test.ts +++ b/apps/web/tests/replayStream.test.ts @@ -79,7 +79,7 @@ describe("startReplayStream", () => { FakeWebSocket.instances[0]!.emitMessage(snapshot); expect(FakeWebSocket.instances[0]!.url).toBe( - "ws://127.0.0.1:8000/ws/spx/0dte/replay?session_id=seed-spx-2026-04-23&at=2026-04-23T15%3A40%3A00Z&source_snapshot_id=snapshot-a" + "ws://127.0.0.1:42180/ws/spx/0dte/replay?session_id=seed-spx-2026-04-23&at=2026-04-23T15%3A40%3A00Z&source_snapshot_id=snapshot-a" ); expect(onSnapshot).toHaveBeenCalledWith(expect.objectContaining({ mode: "replay", diff --git a/apps/web/tests/scenarioRoute.test.ts b/apps/web/tests/scenarioRoute.test.ts index 0d3491c..92d2612 100644 --- a/apps/web/tests/scenarioRoute.test.ts +++ b/apps/web/tests/scenarioRoute.test.ts @@ -52,7 +52,7 @@ describe("POST /api/spx/0dte/scenario", () => { await expect(response.json()).resolves.toEqual(JSON.parse(JSON.stringify(snapshot))); expect(response.headers.get("Cache-Control")).toBe("no-store"); - expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:8000/api/spx/0dte/scenario", { + expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:42180/api/spx/0dte/scenario", { method: "POST", cache: "no-store", headers: { diff --git a/apps/web/tests/setupLocalStorage.ts b/apps/web/tests/setupLocalStorage.ts new file mode 100644 index 0000000..91fd9b1 --- /dev/null +++ b/apps/web/tests/setupLocalStorage.ts @@ -0,0 +1,44 @@ +import { beforeEach } from "vitest"; + +class MemoryStorage implements Storage { + private values = new Map(); + + get length() { + return this.values.size; + } + + clear() { + this.values.clear(); + } + + getItem(key: string) { + return this.values.get(key) ?? null; + } + + key(index: number) { + return Array.from(this.values.keys())[index] ?? null; + } + + removeItem(key: string) { + this.values.delete(key); + } + + setItem(key: string, value: string) { + this.values.set(key, value); + } +} + +beforeEach(() => { + if (typeof window === "undefined") { + return; + } + + try { + window.localStorage.clear(); + } catch { + Object.defineProperty(window, "localStorage", { + configurable: true, + value: new MemoryStorage(), + }); + } +}); diff --git a/apps/web/tests/snapshotRoute.test.ts b/apps/web/tests/snapshotRoute.test.ts index 4a81c24..21c8164 100644 --- a/apps/web/tests/snapshotRoute.test.ts +++ b/apps/web/tests/snapshotRoute.test.ts @@ -70,7 +70,7 @@ describe("GET /api/spx/0dte/snapshot/latest", () => { } })); - expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:8000/api/spx/0dte/snapshot/latest", { + expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:42180/api/spx/0dte/snapshot/latest", { cache: "no-store", headers: { Accept: "application/json", @@ -87,7 +87,7 @@ describe("GET /api/spx/0dte/snapshot/latest", () => { const { GET } = await import("../app/api/spx/0dte/snapshot/latest/route"); await GET(new Request("http://localhost/api/spx/0dte/snapshot/latest")); - expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:8000/api/spx/0dte/snapshot/latest", { + expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:42180/api/spx/0dte/snapshot/latest", { cache: "no-store", headers: { Accept: "application/json" diff --git a/apps/web/tests/snapshotStream.test.ts b/apps/web/tests/snapshotStream.test.ts index e7058ea..296121f 100644 --- a/apps/web/tests/snapshotStream.test.ts +++ b/apps/web/tests/snapshotStream.test.ts @@ -58,7 +58,7 @@ function streamSnapshot(): AnalyticsSnapshot { describe("snapshotWebSocketUrl", () => { it("builds the default local FastAPI websocket URL", () => { - expect(snapshotWebSocketUrl()).toBe("ws://127.0.0.1:8000/ws/spx/0dte"); + expect(snapshotWebSocketUrl()).toBe("ws://127.0.0.1:42180/ws/spx/0dte"); }); it("converts http API bases to websocket URLs", () => { @@ -108,7 +108,7 @@ describe("startSnapshotStream", () => { FakeWebSocket.instances[0]!.emitMessage(snapshot); - expect(FakeWebSocket.instances[0]!.url).toBe("ws://127.0.0.1:8000/ws/spx/0dte"); + expect(FakeWebSocket.instances[0]!.url).toBe("ws://127.0.0.1:42180/ws/spx/0dte"); expect(applySnapshot).toHaveBeenCalledWith(snapshot); }); diff --git a/apps/web/tests/viewsRoute.test.ts b/apps/web/tests/viewsRoute.test.ts index 97a1ff8..2d13a86 100644 --- a/apps/web/tests/viewsRoute.test.ts +++ b/apps/web/tests/viewsRoute.test.ts @@ -39,7 +39,7 @@ describe("/api/views", () => { await expect(response.json()).resolves.toEqual(views); expect(response.headers.get("Cache-Control")).toBe("no-store"); - expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:8000/api/views", { + expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:42180/api/views", { cache: "no-store", headers: { Accept: "application/json" @@ -60,7 +60,7 @@ describe("/api/views", () => { await expect(response.json()).resolves.toEqual(view); expect(response.headers.get("Cache-Control")).toBe("no-store"); - expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:8000/api/views", { + expect(fetcher).toHaveBeenCalledWith("http://127.0.0.1:42180/api/views", { method: "POST", cache: "no-store", headers: { diff --git a/apps/web/vitest.config.ts b/apps/web/vitest.config.ts new file mode 100644 index 0000000..85501a5 --- /dev/null +++ b/apps/web/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + setupFiles: ["./tests/setupLocalStorage.ts"], + }, +}); diff --git a/docker-compose.yml b/docker-compose.yml index d1daeb2..0d04c02 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: POSTGRES_USER: gammascope POSTGRES_PASSWORD: gammascope ports: - - "5432:5432" + - "${GAMMASCOPE_POSTGRES_HOST_PORT:-42432}:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U gammascope -d gammascope"] interval: 5s @@ -16,7 +16,7 @@ services: redis: image: redis:7-alpine ports: - - "6379:6379" + - "${GAMMASCOPE_REDIS_HOST_PORT:-42379}:6379" healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s diff --git a/package.json b/package.json index 0bddd40..b4509f0 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,11 @@ "contracts:generate": "pnpm --filter @gammascope/contracts generate", "test:contracts": "pnpm --filter @gammascope/contracts test", "test:scripts": "node --test tests/package-scripts.test.mjs", - "test:api": "docker compose up -d postgres && .venv/bin/pytest apps/api/tests -q", + "test:api": "docker compose up -d postgres && GAMMASCOPE_DATABASE_URL=postgresql://gammascope:gammascope@127.0.0.1:42432/gammascope .venv/bin/pytest apps/api/tests -q", "test:web": "pnpm --filter @gammascope/web test", "typecheck:web": "pnpm --filter @gammascope/web typecheck", - "dev:web": "pnpm --filter @gammascope/web dev", - "dev:api": ".venv/bin/python -m uvicorn gammascope_api.main:app --reload --app-dir apps/api", + "dev:web": "GAMMASCOPE_API_BASE_URL=http://127.0.0.1:42180 NEXT_PUBLIC_GAMMASCOPE_WS_URL=ws://127.0.0.1:42180/ws/spx/0dte pnpm --filter @gammascope/web exec next dev --hostname 127.0.0.1 --port 42130", + "dev:api": "GAMMASCOPE_DATABASE_URL=postgresql://gammascope:gammascope@127.0.0.1:42432/gammascope GAMMASCOPE_REDIS_URL=redis://127.0.0.1:42379/0 .venv/bin/python -m uvicorn gammascope_api.main:app --reload --app-dir apps/api --host 127.0.0.1 --port 42180", "collector:mock": "PYTHONPATH=services/collector:apps/api .venv/bin/python -m gammascope_collector.cli", "collector:ibkr-health": "PYTHONPATH=services/collector:apps/api .venv/bin/python -m gammascope_collector.ibkr_health", "collector:ibkr-handshake": "PYTHONPATH=services/collector:apps/api .venv/bin/python -m gammascope_collector.ibkr_handshake", diff --git a/services/collector/gammascope_collector/ibkr_config.py b/services/collector/gammascope_collector/ibkr_config.py index 77efb73..95706b2 100644 --- a/services/collector/gammascope_collector/ibkr_config.py +++ b/services/collector/gammascope_collector/ibkr_config.py @@ -13,7 +13,7 @@ class IbkrHealthConfig: client_id: int = 7 collector_id: str = "local-ibkr" account_mode: str = "paper" - api_base: str = "http://127.0.0.1:8000" + api_base: str = "http://127.0.0.1:42180" timeout_seconds: float = 2.0 diff --git a/services/collector/gammascope_collector/moomoo_config.py b/services/collector/gammascope_collector/moomoo_config.py index 4313cad..4e6e7b8 100644 --- a/services/collector/gammascope_collector/moomoo_config.py +++ b/services/collector/gammascope_collector/moomoo_config.py @@ -56,7 +56,7 @@ class MoomooCollectorConfig: refresh_interval_seconds: float = DEFAULT_REFRESH_INTERVAL_SECONDS collector_id: str = "local-moomoo" spx_session_id: str = DEFAULT_SPX_SESSION_ID - api_base: str = "http://127.0.0.1:8000" + api_base: str = "http://127.0.0.1:42180" manual_spots: dict[str, float] = field(default_factory=dict) universe: Sequence[MoomooSymbolConfig] = field(default_factory=tuple) diff --git a/services/collector/gammascope_collector/moomoo_snapshot.py b/services/collector/gammascope_collector/moomoo_snapshot.py index e2f184c..7591e6a 100644 --- a/services/collector/gammascope_collector/moomoo_snapshot.py +++ b/services/collector/gammascope_collector/moomoo_snapshot.py @@ -474,7 +474,7 @@ def main(argv: Sequence[str] | None = None, *, client_factory: ClientFactory | N parser = argparse.ArgumentParser(description="Collect Moomoo 0DTE option snapshots.") parser.add_argument("--host", default="127.0.0.1") parser.add_argument("--port", type=int, default=11111) - parser.add_argument("--api", default="http://127.0.0.1:8000") + parser.add_argument("--api", default="http://127.0.0.1:42180") parser.add_argument("--collector-id", default="local-moomoo") parser.add_argument("--session-id", default=None, help="Stable SPX dashboard session id for published events.") parser.add_argument( diff --git a/services/collector/gammascope_collector/publisher.py b/services/collector/gammascope_collector/publisher.py index a4e2a51..33781ab 100644 --- a/services/collector/gammascope_collector/publisher.py +++ b/services/collector/gammascope_collector/publisher.py @@ -93,7 +93,7 @@ def publish_events_bulk( def main(argv: Sequence[str] | None = None, *, post_json: PostJson | None = None) -> None: parser = argparse.ArgumentParser(description="Publish a mock GammaScope collector cycle to FastAPI.") - parser.add_argument("--api", default="http://127.0.0.1:8000") + parser.add_argument("--api", default="http://127.0.0.1:42180") parser.add_argument("--spot", type=float, required=True) parser.add_argument("--expiry", required=True) parser.add_argument("--strikes", required=True, help="Comma-separated strike list, for example 5190,5200,5210") diff --git a/services/collector/tests/test_ibkr_config.py b/services/collector/tests/test_ibkr_config.py index a74cb8e..c176672 100644 --- a/services/collector/tests/test_ibkr_config.py +++ b/services/collector/tests/test_ibkr_config.py @@ -12,7 +12,7 @@ def test_ibkr_health_config_defaults() -> None: client_id=7, collector_id="local-ibkr", account_mode="paper", - api_base="http://127.0.0.1:8000", + api_base="http://127.0.0.1:42180", timeout_seconds=2.0, ) diff --git a/services/collector/tests/test_moomoo_config.py b/services/collector/tests/test_moomoo_config.py index 55b90eb..5be01b7 100644 --- a/services/collector/tests/test_moomoo_config.py +++ b/services/collector/tests/test_moomoo_config.py @@ -62,7 +62,7 @@ def test_collector_config_defaults_populate_moomoo_universe() -> None: assert config.refresh_interval_seconds == 2.0 assert config.collector_id == "local-moomoo" assert config.spx_session_id == "moomoo-spx-0dte-live" - assert config.api_base == "http://127.0.0.1:8000" + assert config.api_base == "http://127.0.0.1:42180" assert config.manual_spots == {} assert [item.symbol for item in config.universe] == ["SPX", "SPY", "QQQ", "IWM", "RUT", "NDX"] diff --git a/services/collector/tests/test_moomoo_research_recorder.py b/services/collector/tests/test_moomoo_research_recorder.py index b06da80..7b8af36 100644 --- a/services/collector/tests/test_moomoo_research_recorder.py +++ b/services/collector/tests/test_moomoo_research_recorder.py @@ -110,8 +110,12 @@ def test_capture_research_snapshot_once_writes_full_raw_option_fields(tmp_path: assert summary.batch_rows_written == 1 assert summary.underlying_rows_written == 1 assert summary.selected_contracts == {"SPY": 2} - assert summary.option_files == [str(tmp_path / "moomoo/options/date=2026-05-18/ticker=SPY/records.jsonl")] - assert summary.batch_files == [str(tmp_path / "moomoo/batches/date=2026-05-18/ticker=SPY/records.jsonl")] + assert summary.option_files == [ + str(tmp_path / f"moomoo/options/date={summary.market_date}/ticker=SPY/records.jsonl") + ] + assert summary.batch_files == [ + str(tmp_path / f"moomoo/batches/date={summary.market_date}/ticker=SPY/records.jsonl") + ] option_records = _read_jsonl(Path(summary.option_files[0])) assert option_records[0]["source"] == "moomoo" @@ -149,7 +153,7 @@ def test_capture_research_snapshot_once_writes_full_raw_option_fields(tmp_path: assert batch_records[0]["contracts"][0]["raw"]["option_net_open_interest"] == 7 assert batch_records[0]["contracts"][0]["normalized"]["implied_volatility"] == 0.3 - capture_records = _read_jsonl(tmp_path / "moomoo/captures/date=2026-05-18/captures.jsonl") + capture_records = _read_jsonl(tmp_path / f"moomoo/captures/date={summary.market_date}/captures.jsonl") assert capture_records[0]["time_utc"] == capture_records[0]["captured_at_utc"] assert capture_records[0]["time_bucket_utc"] == capture_records[0]["timeline_bucket_utc"] assert capture_records[0]["option_rows_written"] == 2 diff --git a/services/collector/tests/test_publisher.py b/services/collector/tests/test_publisher.py index ec251dc..46375fa 100644 --- a/services/collector/tests/test_publisher.py +++ b/services/collector/tests/test_publisher.py @@ -21,20 +21,20 @@ def test_collector_event_endpoint_joins_api_base() -> None: - assert collector_event_endpoint("http://127.0.0.1:8000") == ( - "http://127.0.0.1:8000/api/spx/0dte/collector/events" + assert collector_event_endpoint("http://127.0.0.1:42180") == ( + "http://127.0.0.1:42180/api/spx/0dte/collector/events" ) - assert collector_event_endpoint("http://127.0.0.1:8000/") == ( - "http://127.0.0.1:8000/api/spx/0dte/collector/events" + assert collector_event_endpoint("http://127.0.0.1:42180/") == ( + "http://127.0.0.1:42180/api/spx/0dte/collector/events" ) def test_collector_events_bulk_endpoint_joins_api_base() -> None: - assert collector_events_bulk_endpoint("http://127.0.0.1:8000") == ( - "http://127.0.0.1:8000/api/spx/0dte/collector/events/bulk" + assert collector_events_bulk_endpoint("http://127.0.0.1:42180") == ( + "http://127.0.0.1:42180/api/spx/0dte/collector/events/bulk" ) - assert collector_events_bulk_endpoint("http://127.0.0.1:8000/") == ( - "http://127.0.0.1:8000/api/spx/0dte/collector/events/bulk" + assert collector_events_bulk_endpoint("http://127.0.0.1:42180/") == ( + "http://127.0.0.1:42180/api/spx/0dte/collector/events/bulk" ) diff --git a/tests/package-scripts.test.mjs b/tests/package-scripts.test.mjs index a57c780..4460f34 100644 --- a/tests/package-scripts.test.mjs +++ b/tests/package-scripts.test.mjs @@ -3,13 +3,33 @@ import { readFile } from "node:fs/promises"; import test from "node:test"; const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")); +const dockerCompose = await readFile(new URL("../docker-compose.yml", import.meta.url), "utf8"); test("dev:api uses the project virtualenv python", () => { - assert.match(packageJson.scripts["dev:api"], /^\.venv\/bin\/python -m uvicorn /); + assert.match(packageJson.scripts["dev:api"], /\.venv\/bin\/python -m uvicorn /); +}); + +test("local dev scripts use uncommon localhost ports", () => { + assert.equal( + packageJson.scripts["dev:web"], + "GAMMASCOPE_API_BASE_URL=http://127.0.0.1:42180 NEXT_PUBLIC_GAMMASCOPE_WS_URL=ws://127.0.0.1:42180/ws/spx/0dte pnpm --filter @gammascope/web exec next dev --hostname 127.0.0.1 --port 42130", + ); + assert.equal( + packageJson.scripts["dev:api"], + "GAMMASCOPE_DATABASE_URL=postgresql://gammascope:gammascope@127.0.0.1:42432/gammascope GAMMASCOPE_REDIS_URL=redis://127.0.0.1:42379/0 .venv/bin/python -m uvicorn gammascope_api.main:app --reload --app-dir apps/api --host 127.0.0.1 --port 42180", + ); +}); + +test("local compose maps stateful services to uncommon host ports", () => { + assert.match(dockerCompose, /"\$\{GAMMASCOPE_POSTGRES_HOST_PORT:-42432\}:5432"/); + assert.match(dockerCompose, /"\$\{GAMMASCOPE_REDIS_HOST_PORT:-42379\}:6379"/); }); test("root test script runs API tests with local Postgres", () => { - assert.match(packageJson.scripts["test:api"], /^docker compose up -d postgres && \.venv\/bin\/pytest apps\/api\/tests -q$/); + assert.match( + packageJson.scripts["test:api"], + /^docker compose up -d postgres && GAMMASCOPE_DATABASE_URL=postgresql:\/\/gammascope:gammascope@127\.0\.0\.1:42432\/gammascope \.venv\/bin\/pytest apps\/api\/tests -q$/, + ); assert.match(packageJson.scripts.test, /pnpm test:api/); });