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