Add Kubernetes image + Helm package with in-cluster Postgres
Containerizes the momentum monolith and packages every run mode for
deployment in a single Kubernetes namespace, with PostgreSQL hosted in
the cluster.
- Dockerfile (python:3.11-slim, non-root, writable /app/data) + .dockerignore
- GitHub Actions workflow publishing to ghcr.io/fez5587/momentum-system
- Helm chart (deploy/helm/momentum):
* PostgreSQL StatefulSet + PVC (single source of truth; schema
auto-creates from storage/pg_schema.sql on first connect)
* App Deployment running run_live_paper.py with the embedded dashboard
(replicas: 1, Recreate; the dashboard's write endpoints need the
in-process TradingExecutionService) + Service/Ingress on 8010, plus a
wait-for-postgres initContainer
* CronJobs for journal / eod-replay / nightly-tune (US/Eastern via
spec.timeZone) sharing the app data PVC
* ConfigMap (tuning env) + Secret (DATABASE_URL/broker keys, existingSecret
supported); DATABASE_URL built from the in-cluster Postgres service
* Optional Grafana provisioned against the in-cluster Postgres
- deploy/README.md with install, secrets, and constraints
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JCVMm2AbmZvvxwLTX6UpNR
This commit is contained in:
20 files changed
+1134
No files matched your search
@@ -0,0 +1,48 @@
|
|||||||
|
# VCS / CI
|
||||||
|
.git
|
||||||
|
.github
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Secrets and runtime data (never bake into the image; provided via Secret/PVC)
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
data/
|
||||||
|
*.duckdb
|
||||||
|
*.wal
|
||||||
|
|
||||||
|
# Local Grafana stack (provisioned via the Helm chart instead)
|
||||||
|
grafana/
|
||||||
|
|
||||||
|
# Deploy artifacts don't belong inside the app image
|
||||||
|
deploy/
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Python caches / build / venvs
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.egg-info/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
.venv
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# Tests and coverage
|
||||||
|
tests/
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# Editor / OS cruft
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Large artifacts
|
||||||
|
*.zip
|
||||||
|
archive/
|
||||||
|
uv.lock
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
name: Build and publish image
|
||||||
|
|
||||||
|
# Builds the app container and pushes it to GitHub Container Registry
|
||||||
|
# (ghcr.io/fez5587/momentum-system). The Helm chart's image.repository points
|
||||||
|
# at this registry; set image.tag to a pushed tag (a commit SHA or `latest`).
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- claude/kubernetes-image-package-kkdkom
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Derive image tags
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=sha
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
# Momentum System — container image
|
||||||
|
#
|
||||||
|
# Single image for every run mode (the app is a monolith run from the repo root
|
||||||
|
# with PYTHONPATH=/app, not a pip-installed wheel — see pyproject.toml
|
||||||
|
# `package = false`). The Kubernetes workloads override CMD:
|
||||||
|
# - app Deployment : python run_live_paper.py (loop + embedded dashboard)
|
||||||
|
# - CronJobs : python eod_replay.py / nightly_tune.py / momentum_cli.py journal
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# psycopg2-binary ships its own libpq, so no build toolchain is needed; we only
|
||||||
|
# add a tiny client for healthchecks/debugging and clean up the apt cache.
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends libpq5 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONPATH=/app \
|
||||||
|
PIP_NO_CACHE_DIR=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies first so they cache across code-only changes.
|
||||||
|
COPY requirements.txt ./
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Application code.
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Run as a non-root user that owns the writable data dir (DuckDB research file,
|
||||||
|
# logs, learned_params.json, schwab tokens). In K8s this path is a mounted PVC.
|
||||||
|
RUN useradd --create-home --uid 10001 app \
|
||||||
|
&& mkdir -p /app/data \
|
||||||
|
&& chown -R app:app /app/data
|
||||||
|
USER app
|
||||||
|
|
||||||
|
EXPOSE 8010
|
||||||
|
|
||||||
|
# Default to the headless loop; the chart's app Deployment runs it WITH the
|
||||||
|
# dashboard, and the CronJobs override this entirely.
|
||||||
|
CMD ["python", "run_live_paper.py", "--no-dashboard"]
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# Kubernetes deployment
|
||||||
|
|
||||||
|
A container image and Helm chart that run the whole Momentum System — including
|
||||||
|
its **PostgreSQL datastore** — inside a single Kubernetes namespace.
|
||||||
|
|
||||||
|
## What gets deployed
|
||||||
|
|
||||||
|
| Workload | Kind | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| PostgreSQL | StatefulSet + PVC | Single source of truth. Schema auto-creates on the app's first connection from `storage/pg_schema.sql`. |
|
||||||
|
| App loop + dashboard | Deployment (`replicas: 1`, `Recreate`) | `run_live_paper.py` with the **embedded** dashboard on :8010. Stateful singleton — never scale up. |
|
||||||
|
| journal / eod-replay / nightly-tune | CronJobs | US/Eastern (`spec.timeZone`), weekdays. Share the app's `data/` PVC. |
|
||||||
|
| Grafana | Deployment + Service | Optional (`grafana.enabled`). Datasource auto-points at the in-cluster Postgres. |
|
||||||
|
|
||||||
|
The app loop and the CronJobs run the **same image** (built from the repo
|
||||||
|
`Dockerfile`), just with different commands.
|
||||||
|
|
||||||
|
## Build & publish the image
|
||||||
|
|
||||||
|
Pushes to the working branch / `main` / `v*` tags trigger
|
||||||
|
`.github/workflows/docker-publish.yml`, which builds and pushes to
|
||||||
|
`ghcr.io/fez5587/momentum-system`. Point the chart at a pushed tag via
|
||||||
|
`image.tag` (a commit SHA, or `latest` on the default branch).
|
||||||
|
|
||||||
|
Local build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t momentum:dev .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm install momentum ./deploy/helm/momentum \
|
||||||
|
-n momentum --create-namespace \
|
||||||
|
--set image.tag=latest \
|
||||||
|
--set secrets.alpacaApiKey=YOUR_KEY \
|
||||||
|
--set secrets.alpacaSecretKey=YOUR_SECRET
|
||||||
|
```
|
||||||
|
|
||||||
|
Without Alpaca keys the loop runs in **dry mode** (no live data/orders); the
|
||||||
|
dashboard and database still work.
|
||||||
|
|
||||||
|
### Secrets
|
||||||
|
|
||||||
|
The chart builds `DATABASE_URL` from the in-cluster Postgres service and a
|
||||||
|
password that is generated on first install (or set `postgres.password` to pin
|
||||||
|
it). Broker keys come from `secrets.*`.
|
||||||
|
|
||||||
|
For real keys, prefer an existing Secret over `values.yaml`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n momentum create secret generic momentum-broker \
|
||||||
|
--from-literal=DATABASE_URL=... \
|
||||||
|
--from-literal=ALPACA_API_KEY=... --from-literal=ALPACA_SECRET_KEY=... \
|
||||||
|
--from-literal=APCA_API_KEY_ID=... --from-literal=APCA_API_SECRET_KEY=...
|
||||||
|
helm install momentum ./deploy/helm/momentum -n momentum \
|
||||||
|
--set secrets.existingSecret=momentum-broker
|
||||||
|
```
|
||||||
|
|
||||||
|
When `secrets.existingSecret` is set the chart renders no Secret of its own and
|
||||||
|
expects that one to carry `DATABASE_URL`, the `ALPACA_*`/`APCA_*` keys, and
|
||||||
|
`POSTGRES_PASSWORD`.
|
||||||
|
|
||||||
|
## Reach the dashboard
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n momentum port-forward svc/momentum-app 8010:8010
|
||||||
|
open http://localhost:8010
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `ingress.enabled=true` (with `ingress.host`/`className`) to expose it
|
||||||
|
instead.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm lint deploy/helm/momentum
|
||||||
|
helm template momentum deploy/helm/momentum | less
|
||||||
|
|
||||||
|
# After install:
|
||||||
|
kubectl -n momentum get pods
|
||||||
|
kubectl -n momentum exec sts/momentum-postgres -- psql -U momentum -d momentum -c '\dt'
|
||||||
|
kubectl -n momentum create job --from=cronjob/momentum-nightly-tune tune-test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes & constraints
|
||||||
|
|
||||||
|
- **Singleton loop.** The trading loop holds in-memory state and one broker
|
||||||
|
session; `replicas` must stay `1` and the strategy is `Recreate`.
|
||||||
|
- **Shared `data/` PVC is ReadWriteOnce.** The CronJob pods mount the same PVC
|
||||||
|
as the app (so `nightly_tune.py` can write `learned_params.json` for the loop
|
||||||
|
to read), which schedules them onto the app pod's node. For multi-node
|
||||||
|
clusters, use an RWX StorageClass or split the volumes.
|
||||||
|
- **CronJob `spec.timeZone`** requires Kubernetes ≥ 1.27.
|
||||||
|
- The legacy `grafana/docker-compose.yml` is left in place for local use; the
|
||||||
|
chart provisions its own Grafana against the in-cluster Postgres.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.DS_Store
|
||||||
|
.git
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
*.orig
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: momentum
|
||||||
|
description: Small-cap momentum research & paper-trading system, with an in-cluster PostgreSQL datastore.
|
||||||
|
type: application
|
||||||
|
# Chart version is independent of the app; bump on chart changes.
|
||||||
|
version: 0.1.0
|
||||||
|
# Tracks the application release (pyproject.toml).
|
||||||
|
appVersion: "0.4.0"
|
||||||
|
kubeVersion: ">=1.27.0-0"
|
||||||
|
keywords:
|
||||||
|
- trading
|
||||||
|
- postgres
|
||||||
|
- momentum
|
||||||
|
home: https://github.com/fez5587/momentum-system
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
{
|
||||||
|
"uid": "momentum-live",
|
||||||
|
"title": "Momentum — Live",
|
||||||
|
"tags": ["momentum"],
|
||||||
|
"timezone": "browser",
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"version": 1,
|
||||||
|
"refresh": "5s",
|
||||||
|
"time": { "from": "now/d", "to": "now" },
|
||||||
|
"templating": { "list": [] },
|
||||||
|
"annotations": { "list": [] },
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Account equity",
|
||||||
|
"gridPos": { "h": 4, "w": 6, "x": 0, "y": 0 },
|
||||||
|
"datasource": { "type": "postgres", "uid": "momentum_pg" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "currencyUSD" }, "overrides": [] },
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "none" },
|
||||||
|
"targets": [{
|
||||||
|
"refId": "A",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT (payload_json::jsonb->>'total_equity')::float AS equity FROM events WHERE event_type='account_summary_updated' ORDER BY timestamp DESC LIMIT 1"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Ready signals (today)",
|
||||||
|
"gridPos": { "h": 4, "w": 6, "x": 6, "y": 0 },
|
||||||
|
"datasource": { "type": "postgres", "uid": "momentum_pg" },
|
||||||
|
"fieldConfig": { "defaults": { "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "blue", "value": 1 } ] } }, "overrides": [] },
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "background", "graphMode": "none" },
|
||||||
|
"targets": [{
|
||||||
|
"refId": "A",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT count(DISTINCT payload_json::jsonb->>'symbol')::int AS ready FROM events WHERE event_type='signal_ready'"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Symbols evaluated",
|
||||||
|
"gridPos": { "h": 4, "w": 6, "x": 12, "y": 0 },
|
||||||
|
"datasource": { "type": "postgres", "uid": "momentum_pg" },
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "none" },
|
||||||
|
"targets": [{
|
||||||
|
"refId": "A",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT count(DISTINCT payload_json::jsonb->>'symbol')::int AS evaluated FROM events WHERE event_type='criteria_evaluated'"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"type": "table",
|
||||||
|
"title": "Evaluation board — what it's looking at + the determination",
|
||||||
|
"gridPos": { "h": 14, "w": 14, "x": 0, "y": 4 },
|
||||||
|
"datasource": { "type": "postgres", "uid": "momentum_pg" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {},
|
||||||
|
"overrides": [
|
||||||
|
{ "matcher": { "id": "byName", "options": "status" },
|
||||||
|
"properties": [ { "id": "custom.cellOptions", "value": { "type": "color-background" } },
|
||||||
|
{ "id": "mappings", "value": [
|
||||||
|
{ "type": "value", "options": { "ready": { "color": "green", "index": 0 }, "late": { "color": "yellow", "index": 1 }, "blocked": { "color": "red", "index": 2 } } } ] } ] },
|
||||||
|
{ "matcher": { "id": "byName", "options": "gap" }, "properties": [ { "id": "unit", "value": "percentunit" } ] },
|
||||||
|
{ "matcher": { "id": "byName", "options": "score" }, "properties": [ { "id": "unit", "value": "percent" } ] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": { "sortBy": [ { "displayName": "score", "desc": true } ] },
|
||||||
|
"targets": [{
|
||||||
|
"refId": "A",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT * FROM ( SELECT DISTINCT ON (sym) sym AS symbol, status, score, gap, rvol, reason FROM ( SELECT payload_json::jsonb->>'symbol' sym, payload_json::jsonb->'criteria_results'->>'status' status, (payload_json::jsonb->>'success_score_pct')::float score, (payload_json::jsonb->'criteria_results'->>'gap_pct')::float gap, (payload_json::jsonb->'criteria_results'->>'relative_volume')::float rvol, payload_json::jsonb->'criteria_results'->>'reason' reason, timestamp ts FROM events WHERE event_type='criteria_evaluated' ) t ORDER BY sym, ts DESC ) x ORDER BY score DESC"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"type": "table",
|
||||||
|
"title": "Ready signals",
|
||||||
|
"gridPos": { "h": 7, "w": 10, "x": 14, "y": 4 },
|
||||||
|
"datasource": { "type": "postgres", "uid": "momentum_pg" },
|
||||||
|
"targets": [{
|
||||||
|
"refId": "A",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT timestamp AS time, payload_json::jsonb->>'symbol' AS symbol, message FROM events WHERE event_type='signal_ready' ORDER BY timestamp DESC LIMIT 25"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "table",
|
||||||
|
"title": "Risk events (breaker / backouts)",
|
||||||
|
"gridPos": { "h": 7, "w": 10, "x": 14, "y": 11 },
|
||||||
|
"datasource": { "type": "postgres", "uid": "momentum_pg" },
|
||||||
|
"targets": [{
|
||||||
|
"refId": "A",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT timestamp AS time, payload_json::jsonb->>'rule_type' AS rule, message FROM events WHERE event_type='risk_rule_triggered' ORDER BY timestamp DESC LIMIT 25"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Minute bars ingested",
|
||||||
|
"gridPos": { "h": 7, "w": 12, "x": 0, "y": 18 },
|
||||||
|
"datasource": { "type": "postgres", "uid": "momentum_pg" },
|
||||||
|
"fieldConfig": { "defaults": { "custom": { "drawStyle": "bars", "fillOpacity": 40 } }, "overrides": [] },
|
||||||
|
"targets": [{
|
||||||
|
"refId": "A",
|
||||||
|
"format": "time_series",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT timestamp AS time, (payload_json::jsonb->>'output_count')::float AS minute_rows FROM events WHERE event_type='module_tick' AND payload_json::jsonb->>'module'='ingestion' AND $__timeFilter(timestamp) ORDER BY 1"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Account equity",
|
||||||
|
"gridPos": { "h": 7, "w": 12, "x": 12, "y": 18 },
|
||||||
|
"datasource": { "type": "postgres", "uid": "momentum_pg" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "currencyUSD" }, "overrides": [] },
|
||||||
|
"targets": [{
|
||||||
|
"refId": "A",
|
||||||
|
"format": "time_series",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT timestamp AS time, (payload_json::jsonb->>'total_equity')::float AS equity FROM events WHERE event_type='account_summary_updated' AND $__timeFilter(timestamp) ORDER BY 1"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
Momentum System deployed as release "{{ .Release.Name }}" in namespace "{{ .Release.Namespace }}".
|
||||||
|
|
||||||
|
Components:
|
||||||
|
- PostgreSQL (StatefulSet, single source of truth) : svc/{{ include "momentum.postgresName" . }}:{{ .Values.postgres.port }}
|
||||||
|
- App loop + embedded dashboard (replicas: 1) : svc/{{ include "momentum.appName" . }}:{{ .Values.service.port }}
|
||||||
|
{{- if .Values.cronjobs.enabled }}
|
||||||
|
- CronJobs (journal, eod-replay, nightly-tune) : timeZone {{ .Values.cronjobs.timeZone }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.grafana.enabled }}
|
||||||
|
- Grafana : svc/{{ include "momentum.grafanaName" . }}:{{ .Values.grafana.service.port }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
Reach the dashboard:
|
||||||
|
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ include "momentum.appName" . }} {{ .Values.service.port }}:{{ .Values.service.port }}
|
||||||
|
open http://localhost:{{ .Values.service.port }}
|
||||||
|
|
||||||
|
{{- if not .Values.secrets.existingSecret }}
|
||||||
|
{{- if or (not .Values.secrets.alpacaApiKey) (not .Values.secrets.alpacaSecretKey) }}
|
||||||
|
|
||||||
|
WARNING: Alpaca keys are not set — the loop runs in dry mode (no live data/orders).
|
||||||
|
Supply them: helm upgrade {{ .Release.Name }} ... \
|
||||||
|
--set secrets.alpacaApiKey=YOUR_KEY --set secrets.alpacaSecretKey=YOUR_SECRET
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
The DB schema auto-creates on the app's first connection. Verify:
|
||||||
|
kubectl -n {{ .Release.Namespace }} exec sts/{{ include "momentum.postgresName" . }} -- \
|
||||||
|
psql -U {{ .Values.postgres.username }} -d {{ .Values.postgres.database }} -c '\dt'
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
{{/* Base name, overridable. */}}
|
||||||
|
{{- define "momentum.name" -}}
|
||||||
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{/* Fully qualified release name. */}}
|
||||||
|
{{- define "momentum.fullname" -}}
|
||||||
|
{{- if .Values.fullnameOverride -}}
|
||||||
|
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||||
|
{{- if contains $name .Release.Name -}}
|
||||||
|
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{/* Common labels. */}}
|
||||||
|
{{- define "momentum.labels" -}}
|
||||||
|
app.kubernetes.io/name: {{ include "momentum.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }}
|
||||||
|
{{- if .Chart.AppVersion }}
|
||||||
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{/* Component-scoped selector labels. Call with (dict "ctx" . "component" "app"). */}}
|
||||||
|
{{- define "momentum.selectorLabels" -}}
|
||||||
|
app.kubernetes.io/name: {{ include "momentum.name" .ctx }}
|
||||||
|
app.kubernetes.io/instance: {{ .ctx.Release.Name }}
|
||||||
|
app.kubernetes.io/component: {{ .component }}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{/* Names of shared resources. */}}
|
||||||
|
{{- define "momentum.postgresName" -}}{{ printf "%s-postgres" (include "momentum.fullname" .) }}{{- end -}}
|
||||||
|
{{- define "momentum.appName" -}}{{ printf "%s-app" (include "momentum.fullname" .) }}{{- end -}}
|
||||||
|
{{- define "momentum.grafanaName" -}}{{ printf "%s-grafana" (include "momentum.fullname" .) }}{{- end -}}
|
||||||
|
{{- define "momentum.configMapName" -}}{{ printf "%s-config" (include "momentum.fullname" .) }}{{- end -}}
|
||||||
|
{{- define "momentum.appDataPvcName" -}}{{ printf "%s-app-data" (include "momentum.fullname" .) }}{{- end -}}
|
||||||
|
|
||||||
|
{{/* Name of the Secret holding DB URL + broker keys (existing or chart-managed). */}}
|
||||||
|
{{- define "momentum.secretName" -}}
|
||||||
|
{{- if .Values.secrets.existingSecret -}}
|
||||||
|
{{- .Values.secrets.existingSecret -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{- printf "%s-secrets" (include "momentum.fullname" .) -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Resolve the Postgres password once: explicit value, else reuse the one already
|
||||||
|
stored in the chart-managed Secret (so upgrades don't rotate it), else generate.
|
||||||
|
*/}}
|
||||||
|
{{- define "momentum.postgresPassword" -}}
|
||||||
|
{{- if .Values.postgres.password -}}
|
||||||
|
{{- .Values.postgres.password -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{- $existing := lookup "v1" "Secret" .Release.Namespace (printf "%s-secrets" (include "momentum.fullname" .)) -}}
|
||||||
|
{{- if and $existing $existing.data (index $existing.data "POSTGRES_PASSWORD") -}}
|
||||||
|
{{- index $existing.data "POSTGRES_PASSWORD" | b64dec -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{- randAlphaNum 24 -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{{- if .Values.app.persistence.enabled }}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: {{ include "momentum.appDataPvcName" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "momentum.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
accessModes: ["{{ .Values.app.persistence.accessMode }}"]
|
||||||
|
{{- if .Values.app.persistence.storageClass }}
|
||||||
|
storageClassName: {{ .Values.app.persistence.storageClass | quote }}
|
||||||
|
{{- end }}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ .Values.app.persistence.size | quote }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "momentum.appName" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "momentum.labels" . | nindent 4 }}
|
||||||
|
app.kubernetes.io/component: app
|
||||||
|
spec:
|
||||||
|
# Stateful singleton: the loop holds in-memory trading state and a 1:1 broker
|
||||||
|
# session. Never scale > 1. Recreate avoids two loops overlapping on rollout.
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "momentum.selectorLabels" (dict "ctx" . "component" "app") | nindent 6 }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
{{- include "momentum.selectorLabels" (dict "ctx" . "component" "app") | nindent 8 }}
|
||||||
|
annotations:
|
||||||
|
# Roll the pod when config/secret content changes.
|
||||||
|
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
|
||||||
|
spec:
|
||||||
|
{{- with .Values.imagePullSecrets }}
|
||||||
|
imagePullSecrets:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
initContainers:
|
||||||
|
# Block until Postgres accepts connections so the first app connection
|
||||||
|
# (which auto-applies storage/pg_schema.sql) succeeds.
|
||||||
|
- name: wait-for-postgres
|
||||||
|
image: {{ .Values.postgres.image | quote }}
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
until pg_isready -h {{ include "momentum.postgresName" . }} -p {{ .Values.postgres.port }} -U {{ .Values.postgres.username }}; do
|
||||||
|
echo "waiting for postgres..."; sleep 2;
|
||||||
|
done
|
||||||
|
containers:
|
||||||
|
- name: app
|
||||||
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
command: {{ toYaml .Values.app.command | nindent 12 }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 8010
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: {{ include "momentum.configMapName" . }}
|
||||||
|
- secretRef:
|
||||||
|
name: {{ include "momentum.secretName" . }}
|
||||||
|
volumeMounts:
|
||||||
|
- name: app-data
|
||||||
|
mountPath: /app/data
|
||||||
|
{{- if .Values.app.probes.enabled }}
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: {{ .Values.app.probes.initialDelaySeconds }}
|
||||||
|
periodSeconds: {{ .Values.app.probes.periodSeconds }}
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: {{ .Values.app.probes.initialDelaySeconds }}
|
||||||
|
periodSeconds: {{ .Values.app.probes.periodSeconds }}
|
||||||
|
{{- end }}
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.app.resources | nindent 12 }}
|
||||||
|
volumes:
|
||||||
|
- name: app-data
|
||||||
|
{{- if .Values.app.persistence.enabled }}
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ include "momentum.appDataPvcName" . }}
|
||||||
|
{{- else }}
|
||||||
|
emptyDir: {}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "momentum.appName" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "momentum.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.service.type }}
|
||||||
|
selector:
|
||||||
|
{{- include "momentum.selectorLabels" (dict "ctx" . "component" "app") | nindent 4 }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: {{ .Values.service.port }}
|
||||||
|
targetPort: http
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: {{ include "momentum.configMapName" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "momentum.labels" . | nindent 4 }}
|
||||||
|
data:
|
||||||
|
{{- range $key, $val := .Values.config }}
|
||||||
|
{{ $key }}: {{ $val | quote }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{{- if .Values.cronjobs.enabled }}
|
||||||
|
{{- $root := . }}
|
||||||
|
{{- $jobs := dict "journal" .Values.cronjobs.journal "eod-replay" .Values.cronjobs.eodReplay "nightly-tune" .Values.cronjobs.nightlyTune }}
|
||||||
|
{{- range $key, $job := $jobs }}
|
||||||
|
{{- if $job.enabled }}
|
||||||
|
---
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: CronJob
|
||||||
|
metadata:
|
||||||
|
name: {{ include "momentum.fullname" $root }}-{{ $key }}
|
||||||
|
labels:
|
||||||
|
{{- include "momentum.labels" $root | nindent 4 }}
|
||||||
|
app.kubernetes.io/component: cron-{{ $key }}
|
||||||
|
spec:
|
||||||
|
schedule: {{ $job.schedule | quote }}
|
||||||
|
# US/Eastern so the market-clock schedules in .env's *.sh comments hold.
|
||||||
|
timeZone: {{ $root.Values.cronjobs.timeZone | quote }}
|
||||||
|
concurrencyPolicy: Forbid
|
||||||
|
successfulJobsHistoryLimit: 3
|
||||||
|
failedJobsHistoryLimit: 3
|
||||||
|
jobTemplate:
|
||||||
|
spec:
|
||||||
|
backoffLimit: 1
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
{{- include "momentum.selectorLabels" (dict "ctx" $root "component" (printf "cron-%s" $key)) | nindent 12 }}
|
||||||
|
spec:
|
||||||
|
restartPolicy: Never
|
||||||
|
{{- with $root.Values.imagePullSecrets }}
|
||||||
|
imagePullSecrets:
|
||||||
|
{{- toYaml . | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
containers:
|
||||||
|
- name: {{ $key }}
|
||||||
|
image: "{{ $root.Values.image.repository }}:{{ $root.Values.image.tag }}"
|
||||||
|
imagePullPolicy: {{ $root.Values.image.pullPolicy }}
|
||||||
|
command: {{ toYaml $job.command | nindent 16 }}
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: {{ include "momentum.configMapName" $root }}
|
||||||
|
- secretRef:
|
||||||
|
name: {{ include "momentum.secretName" $root }}
|
||||||
|
volumeMounts:
|
||||||
|
- name: app-data
|
||||||
|
mountPath: /app/data
|
||||||
|
volumes:
|
||||||
|
- name: app-data
|
||||||
|
{{- if $root.Values.app.persistence.enabled }}
|
||||||
|
# Shared with the app pod (nightly-tune writes learned_params.json
|
||||||
|
# the loop reads on boot). RWO ⇒ cron pods schedule onto the app's node.
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ include "momentum.appDataPvcName" $root }}
|
||||||
|
{{- else }}
|
||||||
|
emptyDir: {}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
{{- if .Values.grafana.enabled }}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: {{ include "momentum.grafanaName" . }}-provisioning
|
||||||
|
labels:
|
||||||
|
{{- include "momentum.labels" . | nindent 4 }}
|
||||||
|
data:
|
||||||
|
# Datasource points at the in-cluster Postgres service (not the old LAN IP).
|
||||||
|
datasource.yaml: |
|
||||||
|
apiVersion: 1
|
||||||
|
datasources:
|
||||||
|
- name: Momentum
|
||||||
|
uid: momentum_pg
|
||||||
|
type: postgres
|
||||||
|
access: proxy
|
||||||
|
url: {{ include "momentum.postgresName" . }}:{{ .Values.postgres.port }}
|
||||||
|
user: {{ .Values.postgres.username }}
|
||||||
|
jsonData:
|
||||||
|
database: {{ .Values.postgres.database }}
|
||||||
|
sslmode: disable
|
||||||
|
postgresVersion: 1600
|
||||||
|
secureJsonData:
|
||||||
|
# Resolved at runtime from the env var below so it always matches the
|
||||||
|
# password stored in the Secret (avoids re-deriving it here).
|
||||||
|
password: $POSTGRES_PASSWORD
|
||||||
|
isDefault: true
|
||||||
|
editable: true
|
||||||
|
provider.yaml: |
|
||||||
|
apiVersion: 1
|
||||||
|
providers:
|
||||||
|
- name: momentum
|
||||||
|
orgId: 1
|
||||||
|
type: file
|
||||||
|
disableDeletion: false
|
||||||
|
updateIntervalSeconds: 10
|
||||||
|
allowUiUpdates: true
|
||||||
|
options:
|
||||||
|
path: /etc/grafana/provisioning/dashboards
|
||||||
|
foldersFromFilesStructure: false
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: {{ include "momentum.grafanaName" . }}-dashboards
|
||||||
|
labels:
|
||||||
|
{{- include "momentum.labels" . | nindent 4 }}
|
||||||
|
data:
|
||||||
|
momentum.json: |-
|
||||||
|
{{- .Files.Get "files/dashboard.json" | nindent 4 }}
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "momentum.grafanaName" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "momentum.labels" . | nindent 4 }}
|
||||||
|
app.kubernetes.io/component: grafana
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "momentum.selectorLabels" (dict "ctx" . "component" "grafana") | nindent 6 }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
{{- include "momentum.selectorLabels" (dict "ctx" . "component" "grafana") | nindent 8 }}
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: grafana
|
||||||
|
image: {{ .Values.grafana.image | quote }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 3000
|
||||||
|
env:
|
||||||
|
- name: GF_AUTH_ANONYMOUS_ENABLED
|
||||||
|
value: "true"
|
||||||
|
- name: GF_AUTH_ANONYMOUS_ORG_ROLE
|
||||||
|
value: "Admin"
|
||||||
|
- name: GF_AUTH_DISABLE_LOGIN_FORM
|
||||||
|
value: "true"
|
||||||
|
- name: GF_DASHBOARDS_MIN_REFRESH_INTERVAL
|
||||||
|
value: "2s"
|
||||||
|
# Interpolated into the provisioned datasource (secureJsonData.password).
|
||||||
|
- name: POSTGRES_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ include "momentum.secretName" . }}
|
||||||
|
key: POSTGRES_PASSWORD
|
||||||
|
volumeMounts:
|
||||||
|
- name: datasources
|
||||||
|
mountPath: /etc/grafana/provisioning/datasources
|
||||||
|
- name: dashboard-provider
|
||||||
|
mountPath: /etc/grafana/provisioning/dashboards/provider.yaml
|
||||||
|
subPath: provider.yaml
|
||||||
|
- name: dashboards
|
||||||
|
mountPath: /etc/grafana/provisioning/dashboards/momentum.json
|
||||||
|
subPath: momentum.json
|
||||||
|
- name: storage
|
||||||
|
mountPath: /var/lib/grafana
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.grafana.resources | nindent 12 }}
|
||||||
|
volumes:
|
||||||
|
- name: datasources
|
||||||
|
configMap:
|
||||||
|
name: {{ include "momentum.grafanaName" . }}-provisioning
|
||||||
|
items:
|
||||||
|
- key: datasource.yaml
|
||||||
|
path: datasource.yaml
|
||||||
|
- name: dashboard-provider
|
||||||
|
configMap:
|
||||||
|
name: {{ include "momentum.grafanaName" . }}-provisioning
|
||||||
|
items:
|
||||||
|
- key: provider.yaml
|
||||||
|
path: provider.yaml
|
||||||
|
- name: dashboards
|
||||||
|
configMap:
|
||||||
|
name: {{ include "momentum.grafanaName" . }}-dashboards
|
||||||
|
- name: storage
|
||||||
|
{{- if .Values.grafana.persistence.enabled }}
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ include "momentum.grafanaName" . }}
|
||||||
|
{{- else }}
|
||||||
|
emptyDir: {}
|
||||||
|
{{- end }}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "momentum.grafanaName" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "momentum.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.grafana.service.type }}
|
||||||
|
selector:
|
||||||
|
{{- include "momentum.selectorLabels" (dict "ctx" . "component" "grafana") | nindent 4 }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: {{ .Values.grafana.service.port }}
|
||||||
|
targetPort: http
|
||||||
|
{{- if .Values.grafana.persistence.enabled }}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: {{ include "momentum.grafanaName" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "momentum.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
accessModes: ["ReadWriteOnce"]
|
||||||
|
{{- if .Values.grafana.persistence.storageClass }}
|
||||||
|
storageClassName: {{ .Values.grafana.persistence.storageClass | quote }}
|
||||||
|
{{- end }}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ .Values.grafana.persistence.size | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{{- if .Values.ingress.enabled }}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: {{ include "momentum.appName" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "momentum.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.ingress.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
{{- if .Values.ingress.className }}
|
||||||
|
ingressClassName: {{ .Values.ingress.className }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.ingress.tls }}
|
||||||
|
tls:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
rules:
|
||||||
|
- host: {{ .Values.ingress.host | quote }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: {{ include "momentum.appName" . }}
|
||||||
|
port:
|
||||||
|
number: {{ .Values.service.port }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "momentum.postgresName" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "momentum.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
# Headless: stable DNS for the single StatefulSet pod.
|
||||||
|
clusterIP: None
|
||||||
|
selector:
|
||||||
|
{{- include "momentum.selectorLabels" (dict "ctx" . "component" "postgres") | nindent 4 }}
|
||||||
|
ports:
|
||||||
|
- name: postgres
|
||||||
|
port: {{ .Values.postgres.port }}
|
||||||
|
targetPort: postgres
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: {{ include "momentum.postgresName" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "momentum.labels" . | nindent 4 }}
|
||||||
|
app.kubernetes.io/component: postgres
|
||||||
|
spec:
|
||||||
|
serviceName: {{ include "momentum.postgresName" . }}
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "momentum.selectorLabels" (dict "ctx" . "component" "postgres") | nindent 6 }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
{{- include "momentum.selectorLabels" (dict "ctx" . "component" "postgres") | nindent 8 }}
|
||||||
|
spec:
|
||||||
|
{{- with .Values.imagePullSecrets }}
|
||||||
|
imagePullSecrets:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
containers:
|
||||||
|
- name: postgres
|
||||||
|
image: {{ .Values.postgres.image | quote }}
|
||||||
|
ports:
|
||||||
|
- name: postgres
|
||||||
|
containerPort: {{ .Values.postgres.port }}
|
||||||
|
env:
|
||||||
|
- name: POSTGRES_USER
|
||||||
|
value: {{ .Values.postgres.username | quote }}
|
||||||
|
- name: POSTGRES_DB
|
||||||
|
value: {{ .Values.postgres.database | quote }}
|
||||||
|
- name: POSTGRES_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ include "momentum.secretName" . }}
|
||||||
|
key: POSTGRES_PASSWORD
|
||||||
|
# Keep PGDATA in a subdir so the volume root (lost+found) doesn't clash.
|
||||||
|
- name: PGDATA
|
||||||
|
value: /var/lib/postgresql/data/pgdata
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /var/lib/postgresql/data
|
||||||
|
readinessProbe:
|
||||||
|
exec:
|
||||||
|
command: ["pg_isready", "-U", {{ .Values.postgres.username | quote }}, "-d", {{ .Values.postgres.database | quote }}]
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
exec:
|
||||||
|
command: ["pg_isready", "-U", {{ .Values.postgres.username | quote }}, "-d", {{ .Values.postgres.database | quote }}]
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 15
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.postgres.resources | nindent 12 }}
|
||||||
|
volumeClaimTemplates:
|
||||||
|
- metadata:
|
||||||
|
name: data
|
||||||
|
spec:
|
||||||
|
accessModes: ["{{ .Values.postgres.persistence.accessMode }}"]
|
||||||
|
{{- if .Values.postgres.persistence.storageClass }}
|
||||||
|
storageClassName: {{ .Values.postgres.persistence.storageClass | quote }}
|
||||||
|
{{- end }}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ .Values.postgres.persistence.size | quote }}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{{- if not .Values.secrets.existingSecret }}
|
||||||
|
{{- $pgPassword := include "momentum.postgresPassword" . }}
|
||||||
|
{{- $pgHost := include "momentum.postgresName" . }}
|
||||||
|
{{- $databaseUrl := printf "postgresql://%s:%s@%s:%v/%s" .Values.postgres.username $pgPassword $pgHost .Values.postgres.port .Values.postgres.database }}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: {{ include "momentum.fullname" . }}-secrets
|
||||||
|
labels:
|
||||||
|
{{- include "momentum.labels" . | nindent 4 }}
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
# Consumed by the Postgres StatefulSet.
|
||||||
|
POSTGRES_PASSWORD: {{ $pgPassword | quote }}
|
||||||
|
# Consumed by the app + CronJobs (storage/db_pg.py prefers DATABASE_URL).
|
||||||
|
DATABASE_URL: {{ $databaseUrl | quote }}
|
||||||
|
# Alpaca paper broker (the client reads both the ALPACA_* and APCA_* names).
|
||||||
|
ALPACA_API_KEY: {{ .Values.secrets.alpacaApiKey | quote }}
|
||||||
|
ALPACA_SECRET_KEY: {{ .Values.secrets.alpacaSecretKey | quote }}
|
||||||
|
APCA_API_KEY_ID: {{ .Values.secrets.alpacaApiKey | quote }}
|
||||||
|
APCA_API_SECRET_KEY: {{ .Values.secrets.alpacaSecretKey | quote }}
|
||||||
|
APCA_API_BASE_URL: {{ .Values.secrets.alpacaBaseUrl | quote }}
|
||||||
|
{{- range $k, $v := .Values.secrets.schwab }}
|
||||||
|
{{ $k }}: {{ $v | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
# Default values for the momentum chart.
|
||||||
|
# Override per-environment with `-f my-values.yaml` or `--set key=value`.
|
||||||
|
|
||||||
|
# -- Container image for every workload (app loop, dashboard, CronJobs).
|
||||||
|
image:
|
||||||
|
repository: ghcr.io/fez5587/momentum-system
|
||||||
|
# Pin to a pushed tag (commit SHA or `latest`) from the docker-publish workflow.
|
||||||
|
tag: latest
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
|
# Optional image pull secrets for a private GHCR package.
|
||||||
|
imagePullSecrets: []
|
||||||
|
|
||||||
|
nameOverride: ""
|
||||||
|
fullnameOverride: ""
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# App: the live trading loop WITH its embedded dashboard (port 8010).
|
||||||
|
# It is a stateful singleton — do not raise replicas above 1.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
app:
|
||||||
|
# Runs the loop with the dashboard attached (the dashboard's approve/reject/
|
||||||
|
# exit endpoints require the in-process TradingExecutionService).
|
||||||
|
command: ["python", "run_live_paper.py"]
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
limits:
|
||||||
|
memory: 1Gi
|
||||||
|
# Shared writable dir (DuckDB research file, logs, learned_params.json, tokens).
|
||||||
|
# The nightly-tune CronJob writes learned_params.json here for the loop to read.
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
size: 5Gi
|
||||||
|
storageClass: ""
|
||||||
|
accessMode: ReadWriteOnce
|
||||||
|
# HTTP probes hit the dashboard root.
|
||||||
|
probes:
|
||||||
|
enabled: true
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 30
|
||||||
|
|
||||||
|
# Dashboard service (ClusterIP by default; port-forward to reach it).
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
port: 8010
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
|
className: ""
|
||||||
|
annotations: {}
|
||||||
|
host: momentum.local
|
||||||
|
tls: []
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# In-cluster PostgreSQL — the single source of truth (StatefulSet + PVC).
|
||||||
|
# Schema auto-creates on first app connection from storage/pg_schema.sql.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
postgres:
|
||||||
|
image: postgres:16
|
||||||
|
database: momentum
|
||||||
|
username: momentum
|
||||||
|
# If empty, a random password is generated on first install and stored in the
|
||||||
|
# chart-managed Secret. Set explicitly to keep it stable across upgrades.
|
||||||
|
password: ""
|
||||||
|
port: 5432
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
limits:
|
||||||
|
memory: 1Gi
|
||||||
|
persistence:
|
||||||
|
size: 10Gi
|
||||||
|
storageClass: ""
|
||||||
|
accessMode: ReadWriteOnce
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Secrets — DB URL is built from the in-cluster Postgres service automatically.
|
||||||
|
# Provide broker keys via --set or, better, an existingSecret (sealed-secrets,
|
||||||
|
# external-secrets, etc). Never commit real keys into values.yaml.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
secrets:
|
||||||
|
# If set, the chart will NOT render its own Secret and instead expects this
|
||||||
|
# one to already contain DATABASE_URL, ALPACA_*/APCA_*, and any SCHWAB_* keys.
|
||||||
|
existingSecret: ""
|
||||||
|
alpacaApiKey: ""
|
||||||
|
alpacaSecretKey: ""
|
||||||
|
alpacaBaseUrl: "https://paper-api.alpaca.markets"
|
||||||
|
schwab: {} # e.g. SCHWAB_MARKET_DATA_APP_KEY: "..."
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Non-secret tuning env (ConfigMap). Mirrors .env.example; override freely.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
config:
|
||||||
|
TRADING_EXECUTION_MODE: "alpaca_paper"
|
||||||
|
TRADING_EXECUTION_ENABLED: "1"
|
||||||
|
TRADING_AUTO_APPROVE: "0"
|
||||||
|
TRADING_MAX_CONCURRENT_POSITIONS: "6"
|
||||||
|
TRADING_RISK_PER_TRADE_PCT: "0.01"
|
||||||
|
TRADING_MAX_DAILY_LOSS_PCT: "0.03"
|
||||||
|
TRADING_EOD_FLATTEN_ENABLED: "1"
|
||||||
|
WATCHER_ENABLED: "1"
|
||||||
|
WATCHER_INTERVAL_SECONDS: "60"
|
||||||
|
WATCHER_SYMBOLS: "AAPL,TSLA,NVDA,AMD,SOFI"
|
||||||
|
WATCHER_PRICE_MIN: "1"
|
||||||
|
WATCHER_PRICE_MAX: "20"
|
||||||
|
STRATEGY_SETUPS: "opening_range_break"
|
||||||
|
LIVE_BARS_ENABLED: "1"
|
||||||
|
LIVE_BARS_INTERVAL_SECONDS: "60"
|
||||||
|
ALPACA_DATA_FEED: "iex"
|
||||||
|
ALPACA_BACKFILL_FEED: "sip"
|
||||||
|
TRIGGER_ENABLED: "1"
|
||||||
|
TRIGGER_GAP_MIN: "3.0"
|
||||||
|
TRIGGER_GAP_MAX: "35"
|
||||||
|
TRIGGER_RVOL_MIN: "2.0"
|
||||||
|
ORB_BARS: "5"
|
||||||
|
DASHBOARD_HOST: "0.0.0.0"
|
||||||
|
DASHBOARD_PORT: "8010"
|
||||||
|
# Leave schema auto-creation on (storage/db_pg.py applies pg_schema.sql).
|
||||||
|
PG_SKIP_SCHEMA: "0"
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Scheduled jobs (CronJobs). Times are US/Eastern via spec.timeZone (K8s >=1.27),
|
||||||
|
# weekdays only — matching the existing *.sh crontab comments.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
cronjobs:
|
||||||
|
enabled: true
|
||||||
|
timeZone: America/New_York
|
||||||
|
journal:
|
||||||
|
enabled: true
|
||||||
|
schedule: "2 16 * * 1-5"
|
||||||
|
command: ["python", "momentum_cli.py", "journal"]
|
||||||
|
eodReplay:
|
||||||
|
enabled: true
|
||||||
|
schedule: "5 16 * * 1-5"
|
||||||
|
command: ["python", "eod_replay.py"]
|
||||||
|
nightlyTune:
|
||||||
|
enabled: true
|
||||||
|
schedule: "30 17 * * 1-5"
|
||||||
|
command: ["python", "nightly_tune.py"]
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Grafana (optional) — provisioned against the in-cluster Postgres service.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
grafana:
|
||||||
|
enabled: true
|
||||||
|
image: grafana/grafana:11.2.0
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
port: 3000
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
size: 1Gi
|
||||||
|
storageClass: ""
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 128Mi
|
||||||
|
limits:
|
||||||
|
memory: 512Mi
|
||||||
Reference in new issue
Block a user