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) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-06-08 00:16:36 +02:00
parent 4bbf07bddf
commit dc2b7214f7
2 changed files with 125 additions and 0 deletions

View File

@@ -18,6 +18,9 @@ spec:
- podSelector:
matchLabels:
app.kubernetes.io/name: broker-gateway
- podSelector:
matchLabels:
app: multistrat-rebalancer
ports:
- protocol: TCP
port: 4002

View File

@@ -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