From dc2b7214f75e96a985e10a37915bf77fbde2f388 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Mon, 8 Jun 2026 00:16:36 +0200 Subject: [PATCH] feat(infra): K8s CronJob for multi-strat rebalancer (production deployment) Weekly in-cluster CronJob running multistrat_bot.py on IBKR paper via the existing ib-gateway (connects to ib-gateway:4002 directly, no port-forward). Lean: code via ConfigMap (no image build), python:3.12-slim + runtime pip. Includes PVC (state/HWM persistence across runs), egress NetworkPolicy (ib-gateway:4002 + 443 for pip/Yahoo + DNS), and ib-gateway ingress patched to allow the rebalancer. Schedule Mon 14:35 UTC (~1h after US open). MULTISTRAT_EXECUTE=false by default (cluster dry-run); flip to true after validating logs. Drawdown CB + leverage cap + idempotency + paper-guard enforced by the bot. Co-Authored-By: Claude Opus 4.8 (1M context) --- infra/k8s/network-policies/ib-gateway.yaml | 3 + infra/k8s/services/multistrat-rebalancer.yaml | 122 ++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 infra/k8s/services/multistrat-rebalancer.yaml diff --git a/infra/k8s/network-policies/ib-gateway.yaml b/infra/k8s/network-policies/ib-gateway.yaml index 57632a518..cbed5e445 100644 --- a/infra/k8s/network-policies/ib-gateway.yaml +++ b/infra/k8s/network-policies/ib-gateway.yaml @@ -18,6 +18,9 @@ spec: - podSelector: matchLabels: app.kubernetes.io/name: broker-gateway + - podSelector: + matchLabels: + app: multistrat-rebalancer ports: - protocol: TCP port: 4002 diff --git a/infra/k8s/services/multistrat-rebalancer.yaml b/infra/k8s/services/multistrat-rebalancer.yaml new file mode 100644 index 000000000..1b75aa18d --- /dev/null +++ b/infra/k8s/services/multistrat-rebalancer.yaml @@ -0,0 +1,122 @@ +# Multi-strat rebalancer — weekly CronJob running the adaptive book on IBKR (paper) via the +# in-cluster ib-gateway. Production deployment of scripts/surfer/multistrat_bot.py. +# +# Code is delivered via ConfigMap (no image build needed — lean). Create/refresh it with: +# kubectl create configmap multistrat-bot-code -n foxhunt \ +# --from-file=scripts/surfer/multistrat_bot.py \ +# --from-file=scripts/surfer/multistrat_paper.py \ +# --dry-run=client -o yaml | kubectl apply -f - +# +# SAFETY: MULTISTRAT_EXECUTE defaults to "false" (cluster dry-run). After validating a few dry-run +# CronJob logs, flip to "true" to place paper orders. Account is paper (ib-gateway TRADING_MODE=paper); +# the bot additionally refuses any non-DU (live) account unless MULTISTRAT_ALLOW_LIVE_CONFIRMED is set. +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: multistrat-state + namespace: foxhunt + labels: + app: multistrat-rebalancer +spec: + accessModes: ["ReadWriteOnce"] # sequential CronJob runs (Forbid concurrency) — RWO is fine + resources: + requests: + storage: 1Gi +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: multistrat-rebalancer + namespace: foxhunt +spec: + podSelector: + matchLabels: + app: multistrat-rebalancer + policyTypes: [Egress] + egress: + - to: [] # DNS + ports: + - {protocol: UDP, port: 53} + - {protocol: TCP, port: 53} + - to: # in-cluster ib-gateway + - podSelector: + matchLabels: + app: ib-gateway + ports: + - {protocol: TCP, port: 4002} + - to: # internet 443: PyPI (pip) + Yahoo (prices), no internal ranges + - ipBlock: + cidr: 0.0.0.0/0 + except: ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] + ports: + - {protocol: TCP, port: 443} +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: multistrat-rebalancer + namespace: foxhunt + labels: + app: multistrat-rebalancer + app.kubernetes.io/part-of: foxhunt +spec: + schedule: "35 14 * * 1" # Monday 14:35 UTC (~1h after US equity open 13:30 UTC) + concurrencyPolicy: Forbid + startingDeadlineSeconds: 3600 + successfulJobsHistoryLimit: 5 + failedJobsHistoryLimit: 10 + jobTemplate: + spec: + backoffLimit: 1 + activeDeadlineSeconds: 600 + template: + metadata: + labels: + app: multistrat-rebalancer + spec: + restartPolicy: Never + nodeSelector: + k8s.scaleway.com/pool-name: platform + securityContext: + seccompProfile: + type: RuntimeDefault + containers: + - name: rebalancer + image: python:3.12-slim + command: ["/bin/sh", "-c"] + args: + - >- + pip install --quiet --no-cache-dir ib_async==2.1.0 numpy && + python /app/multistrat_bot.py run + env: + - {name: IB_HOST, value: "ib-gateway"} + - {name: IB_PORT, value: "4002"} + - {name: IB_CLIENT_ID, value: "11"} + - {name: MULTISTRAT_MAXLEV, value: "1.0"} + - {name: MULTISTRAT_HYST, value: "0.03"} + - {name: MULTISTRAT_REBALANCE_DAYS, value: "7"} + - {name: MULTISTRAT_DD_HALT, value: "0.20"} + - {name: MULTISTRAT_MAX_ORDER, value: "0.30"} + - {name: MULTISTRAT_EXECUTE, value: "false"} # <-- flip to "true" after validating dry-run logs + - {name: MULTISTRAT_STATE, value: "/data/state.json"} + - {name: MULTISTRAT_LOG, value: "/data/bot.log"} + - {name: PIP_DISABLE_PIP_VERSION_CHECK, value: "1"} + - {name: PYTHONUNBUFFERED, value: "1"} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + resources: + requests: {memory: "256Mi", cpu: "100m"} + limits: {memory: "512Mi", cpu: "500m"} + volumeMounts: + - {mountPath: /app, name: code, readOnly: true} + - {mountPath: /data, name: state} + volumes: + - name: code + configMap: + name: multistrat-bot-code + - name: state + persistentVolumeClaim: + claimName: multistrat-state