diff --git a/trading-platform/deploy/ci-cd/.github/workflows/ci-cd.yml b/trading-platform/deploy/ci-cd/.github/workflows/ci-cd.yml index b6993ee..72386b2 100644 --- a/trading-platform/deploy/ci-cd/.github/workflows/ci-cd.yml +++ b/trading-platform/deploy/ci-cd/.github/workflows/ci-cd.yml @@ -34,7 +34,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - service: [execute-service, data-service, news-service] + service: [execute-service, data-service, news-service, solana-quant-bot] defaults: run: working-directory: trading-platform/${{ matrix.service }} @@ -109,7 +109,7 @@ jobs: if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' strategy: matrix: - service: [execute-service, data-service, news-service, api-gateway, dashboard] + service: [execute-service, data-service, news-service, api-gateway, dashboard, solana-quant-bot] steps: - uses: actions/checkout@v4 @@ -184,6 +184,7 @@ jobs: kubectl rollout status deployment/news-service -n customer1-staging --timeout=5m kubectl rollout status deployment/api-gateway -n customer1-staging --timeout=5m kubectl rollout status deployment/dashboard -n customer1-staging --timeout=5m + kubectl rollout status deployment/solana-quant-bot -n customer1-staging --timeout=5m deploy-production: needs: [deploy-staging] diff --git a/trading-platform/helm/trading-platform/templates/configmap.yaml b/trading-platform/helm/trading-platform/templates/configmap.yaml index 1e1ce4e..25f31ee 100644 --- a/trading-platform/helm/trading-platform/templates/configmap.yaml +++ b/trading-platform/helm/trading-platform/templates/configmap.yaml @@ -19,3 +19,7 @@ data: # PostgreSQL connection POSTGRES_HOST: postgres-primary.{{ .Values.global.namespace }}.svc.cluster.local POSTGRES_PORT: "5432" + # Solana RPC (ExternalName service) + {{- if .Values.solanaRpc.enabled }} + SOLANA_RPC_URL: "http://solana-rpc.{{ .Values.global.namespace }}.svc.cluster.local:{{ .Values.solanaRpc.port }}" + {{- end }} diff --git a/trading-platform/helm/trading-platform/templates/network-policies/network-policies.yaml b/trading-platform/helm/trading-platform/templates/network-policies/network-policies.yaml index 76fb484..2f1fac6 100644 --- a/trading-platform/helm/trading-platform/templates/network-policies/network-policies.yaml +++ b/trading-platform/helm/trading-platform/templates/network-policies/network-policies.yaml @@ -179,6 +179,9 @@ spec: - podSelector: matchLabels: app: data-service + - podSelector: + matchLabels: + app: solana-quant-bot ports: - port: 5432 protocol: TCP @@ -204,6 +207,9 @@ spec: - podSelector: matchLabels: app: data-service + - podSelector: + matchLabels: + app: solana-quant-bot ports: - port: 6379 protocol: TCP @@ -231,6 +237,9 @@ spec: - podSelector: matchLabels: app: data-service + - podSelector: + matchLabels: + app: solana-quant-bot ports: - port: 9092 protocol: TCP @@ -308,4 +317,93 @@ spec: protocol: TCP --- {{- end }} +# Solana Quant Bot networking +{{- if .Values.solanaQuantBot.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: solana-quant-bot-networking + namespace: {{ .Values.global.namespace }} +spec: + podSelector: + matchLabels: + app: solana-quant-bot + policyTypes: + - Ingress + - Egress + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: ingress-nginx + ports: + - port: {{ .Values.solanaQuantBot.port }} + protocol: TCP + - port: {{ .Values.solanaQuantBot.metricsPort }} + protocol: TCP + egress: + # PostgreSQL + - to: + - podSelector: + matchLabels: + app: postgres + ports: + - port: 5432 + protocol: TCP + # Redis + - to: + - podSelector: + matchLabels: + app: redis + ports: + - port: 6379 + protocol: TCP + # Kafka + - to: + - podSelector: + matchLabels: + app: kafka + ports: + - port: 9092 + protocol: TCP + # Solana RPC (ExternalName — allows outbound to external RPC provider) + - to: + - podSelector: + matchLabels: + app: solana-rpc + ports: + - port: {{ .Values.solanaRpc.port }} + protocol: TCP + # External RPC provider (Helius/QuickNode — outbound HTTPS) + - to: [] + ports: + - port: 443 + protocol: TCP + - port: 80 + protocol: TCP +--- +{{- end }} +# Solana RPC networking (ExternalName allows outbound to external provider) +{{- if .Values.solanaRpc.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: solana-rpc-networking + namespace: {{ .Values.global.namespace }} +spec: + podSelector: + matchLabels: + app: solana-rpc + policyTypes: + - Egress + egress: + # Allow outbound HTTPS to external RPC provider + - to: [] + ports: + - port: 443 + protocol: TCP + - port: 80 + protocol: TCP +--- +{{- end }} {{- end }} diff --git a/trading-platform/helm/trading-platform/templates/observability/solana-quant-bot-grafana-dashboard.yaml b/trading-platform/helm/trading-platform/templates/observability/solana-quant-bot-grafana-dashboard.yaml new file mode 100644 index 0000000..51aad77 --- /dev/null +++ b/trading-platform/helm/trading-platform/templates/observability/solana-quant-bot-grafana-dashboard.yaml @@ -0,0 +1,139 @@ +{{- if .Values.solanaQuantBot.enabled }} +{{- if .Values.grafana.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.solanaQuantBot.name }}-grafana-dashboard + namespace: {{ .Values.grafana.namespace | default .Values.global.namespace }} + labels: + {{- include "trading-platform.labels" . | nindent 4 }} + app.kubernetes.io/part-of: solana-quant-bot + grafana_dashboard: "1" +data: + solana-quant-bot-dashboard.json: | + { + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "title": "Bot Overview", + "type": "row", + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 0}, + "collapsed": false + }, + { + "title": "Pods Running", + "type": "stat", + "gridPos": {"h": 4, "w": 6, "x": 0, "y": 1}, + "targets": [ + {"expr": "up{job=\"solana-quant-bot\"}", "legendFormat": "{{instance}}"} + ], + "fieldConfig": { + "defaults": { + "thresholds": {"steps": [{"color": "red", "value": null}, {"color": "green", "value": 1}]}, + "unit": "short" + } + } + }, + { + "title": "Total Trades (24h)", + "type": "stat", + "gridPos": {"h": 4, "w": 6, "x": 6, "y": 1}, + "targets": [ + {"expr": "increase(solana_quant_bot_trades_total[24h])"} + ], + "fieldConfig": {"defaults": {"unit": "short"}} + }, + { + "title": "Trade Error Rate (5m)", + "type": "stat", + "gridPos": {"h": 4, "w": 6, "x": 12, "y": 1}, + "targets": [ + {"expr": "sum(rate(solana_quant_bot_trade_errors_total[5m])) / sum(rate(solana_quant_bot_trades_total[5m])) * 100"} + ], + "fieldConfig": { + "defaults": { + "unit": "percent", + "thresholds": {"steps": [{"color": "green", "value": null}, {"color": "yellow", "value": 3}, {"color": "red", "value": 10}]} + } + } + }, + { + "title": "SOL Balance", + "type": "stat", + "gridPos": {"h": 4, "w": 6, "x": 18, "y": 1}, + "targets": [ + {"expr": "solana_quant_bot_sol_balance"} + ], + "fieldConfig": { + "defaults": { + "unit": "short", + "thresholds": {"steps": [{"color": "red", "value": null}, {"color": "orange", "value": 10}, {"color": "green", "value": 25}]} + } + } + }, + { + "title": "Trade Volume (USDC)", + "type": "timeseries", + "gridPos": {"h": 8, "w": 24, "x": 0, "y": 5}, + "targets": [ + {"expr": "sum(rate(solana_quant_bot_trade_volume_usdc[5m])) * 60", "legendFormat": "USDC/min"} + ] + }, + { + "title": "Signal Processing Latency", + "type": "timeseries", + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 13}, + "targets": [ + {"expr": "histogram_quantile(0.50, sum(rate(solana_quant_bot_signal_latency_seconds_bucket[5m])) by (le))", "legendFormat": "p50"}, + {"expr": "histogram_quantile(0.95, sum(rate(solana_quant_bot_signal_latency_seconds_bucket[5m])) by (le))", "legendFormat": "p95"}, + {"expr": "histogram_quantile(0.99, sum(rate(solana_quant_bot_signal_latency_seconds_bucket[5m])) by (le))", "legendFormat": "p99"} + ], + "fieldConfig": {"defaults": {"unit": "s"}} + }, + { + "title": "Trades per Minute", + "type": "timeseries", + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 13}, + "targets": [ + {"expr": "sum(rate(solana_quant_bot_trades_total[5m])) * 60", "legendFormat": "trades/min"} + ] + }, + { + "title": "Token Score Distribution", + "type": "timeseries", + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 21}, + "targets": [ + {"expr": "avg(solana_quant_bot_token_score)", "legendFormat": "avg score"}, + {"expr": "max(solana_quant_bot_token_score)", "legendFormat": "max score"} + ] + }, + { + "title": "CPU / Memory Usage", + "type": "timeseries", + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 21}, + "targets": [ + {"expr": "sum(rate(container_cpu_usage_seconds_total{pod=~\"solana-quant-bot-.*\"}[5m])) * 1000", "legendFormat": "CPU (m)"}, + {"expr": "sum(container_memory_usage_bytes{pod=~\"solana-quant-bot-.*\"}) / 1024 / 1024", "legendFormat": "Memory (MB)"} + ], + "fieldConfig": {"defaults": {"unit": "short"}} + } + ], + "refresh": "30s", + "schemaVersion": 39, + "tags": ["solana", "quant-bot", "trading"], + "templating": {"list": []}, + "time": {"from": "now-3h", "to": "now"}, + "title": "Solana Quant Bot", + "uid": "solana-quant-bot", + "version": 1 + } +{{- end }} +{{- end }} diff --git a/trading-platform/helm/trading-platform/templates/observability/solana-quant-bot-prometheus-rules.yaml b/trading-platform/helm/trading-platform/templates/observability/solana-quant-bot-prometheus-rules.yaml new file mode 100644 index 0000000..4393258 --- /dev/null +++ b/trading-platform/helm/trading-platform/templates/observability/solana-quant-bot-prometheus-rules.yaml @@ -0,0 +1,70 @@ +{{- if .Values.solanaQuantBot.enabled }} +{{- if .Values.alertmanager.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: {{ .Values.solanaQuantBot.name }}-alerts + namespace: {{ .Values.global.namespace }} + labels: + {{- include "trading-platform.labels" . | nindent 4 }} + app.kubernetes.io/part-of: solana-quant-bot + release: prometheus +spec: + groups: + - name: solana-quant-bot.rules + rules: + # Alert if bot pod is down + - alert: SolanaQuantBotDown + expr: up{job="{{ .Values.solanaQuantBot.name }}"} == 0 + for: 5m + labels: + severity: critical + annotations: + summary: "Solana Quant Bot instance {{ '{{ $instance }}' }} is down" + description: "Bot has been unreachable for more than 5 minutes." + + # High error rate on trades + - alert: SolanaQuantBotHighErrorRate + expr: | + sum(rate(solana_quant_bot_trade_errors_total[5m])) + / sum(rate(solana_quant_bot_trades_total[5m])) > 0.05 + for: 10m + labels: + severity: warning + annotations: + summary: "Solana Quant Bot trade error rate is >5%" + description: "Error rate: {{ '{{ $value }}' }} over 5m window." + + # No trades executed in a long time (stale bot) + - alert: SolanaQuantBotNoTrades + expr: | + time() - increase(solana_quant_bot_last_trade_timestamp[1h]) > 3600 + for: 15m + labels: + severity: warning + annotations: + summary: "Solana Quant Bot has not executed trades in 1 hour" + description: "Bot may be stuck or signals pipeline is blocked." + + # High signal processing latency + - alert: SolanaQuantBotHighLatency + expr: | + histogram_quantile(0.95, sum(rate(solana_quant_bot_signal_latency_seconds_bucket[5m])) by (le)) > 2 + for: 10m + labels: + severity: warning + annotations: + summary: "Solana Quant Bot p95 signal latency > 2s" + description: "p95 latency: {{ '{{ $value }}' }}s." + + # Wallet balance below threshold + - alert: SolanaQuantBotLowBalance + expr: solana_quant_bot_sol_balance < 10 + for: 5m + labels: + severity: critical + annotations: + summary: "Solana Quant Bot SOL balance is below 10 SOL" + description: "Current balance: {{ '{{ $value }}' }} SOL." +{{- end }} +{{- end }} diff --git a/trading-platform/helm/trading-platform/templates/observability/solana-quant-bot-service-monitor.yaml b/trading-platform/helm/trading-platform/templates/observability/solana-quant-bot-service-monitor.yaml new file mode 100644 index 0000000..ccb0201 --- /dev/null +++ b/trading-platform/helm/trading-platform/templates/observability/solana-quant-bot-service-monitor.yaml @@ -0,0 +1,25 @@ +{{- if .Values.solanaQuantBot.enabled }} +{{- if .Values.prometheus.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ .Values.solanaQuantBot.name }} + namespace: {{ .Values.global.namespace }} + labels: + {{- include "trading-platform.labels" . | nindent 4 }} + app.kubernetes.io/part-of: solana-quant-bot + release: prometheus +spec: + selector: + matchLabels: + app: {{ .Values.solanaQuantBot.name }} + endpoints: + - port: metrics + path: /metrics + interval: {{ .Values.prometheus.scrapeInterval | default "15s" }} + scrapeTimeout: 10s + namespaceSelector: + matchNames: + - {{ .Values.global.namespace }} +{{- end }} +{{- end }} diff --git a/trading-platform/helm/trading-platform/templates/solana-quant-bot/alembic-migration-job.yaml b/trading-platform/helm/trading-platform/templates/solana-quant-bot/alembic-migration-job.yaml new file mode 100644 index 0000000..437c45b --- /dev/null +++ b/trading-platform/helm/trading-platform/templates/solana-quant-bot/alembic-migration-job.yaml @@ -0,0 +1,58 @@ +{{- if .Values.solanaQuantBot.enabled }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ .Values.solanaQuantBot.name }}-db-migrate + namespace: {{ .Values.global.namespace }} + labels: + {{- include "trading-platform.labels" . | nindent 4 }} + app: {{ .Values.solanaQuantBot.name }} + app.kubernetes.io/part-of: solana-quant-bot + annotations: + # Prevent re-running on helm upgrade if already succeeded + "helm.sh/hook": post-install,post-upgrade + "helm.sh/hook-weight": "-5" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +spec: + template: + metadata: + labels: + app: {{ .Values.solanaQuantBot.name }} + app.kubernetes.io/part-of: solana-quant-bot + spec: + serviceAccountName: {{ .Values.solanaQuantBot.name }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: alembic-migrate + image: "{{ .Values.solanaQuantBot.image.repository }}:{{ .Values.solanaQuantBot.image.tag }}" + imagePullPolicy: {{ .Values.solanaQuantBot.image.pullPolicy }} + command: ["alembic"] + args: ["upgrade", "head"] + env: + - name: DB_HOST + valueFrom: + configMapKeyRef: + name: {{ include "trading-platform.fullname" . }}-config + key: POSTGRES_HOST + - name: DB_PORT + value: "5432" + - name: DB_NAME + value: {{ .Values.solanaQuantBot.env.DB_NAME | quote }} + envFrom: + - secretRef: + name: trading-secrets + resources: + {{- toYaml .Values.solanaQuantBot.migrationResources | nindent 12 }} + securityContext: + {{- toYaml .Values.containerSecurityContext | nindent 12 }} + restartPolicy: Never + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + nodeSelector: + kubernetes.io/os: linux + backoffLimit: 3 + activeDeadlineSeconds: 300 +{{- end }} diff --git a/trading-platform/helm/trading-platform/templates/solana-quant-bot/deployment.yaml b/trading-platform/helm/trading-platform/templates/solana-quant-bot/deployment.yaml new file mode 100644 index 0000000..eb31d97 --- /dev/null +++ b/trading-platform/helm/trading-platform/templates/solana-quant-bot/deployment.yaml @@ -0,0 +1,138 @@ +{{- if .Values.solanaQuantBot.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Values.solanaQuantBot.name }} + namespace: {{ .Values.global.namespace }} + labels: + {{- include "trading-platform.labels" . | nindent 4 }} + app: {{ .Values.solanaQuantBot.name }} + app.kubernetes.io/part-of: solana-quant-bot +spec: + replicas: {{ .Values.solanaQuantBot.replicaCount }} + selector: + matchLabels: + {{- include "trading-platform.selectorLabels" . | nindent 6 }} + app: {{ .Values.solanaQuantBot.name }} + template: + metadata: + labels: + {{- include "trading-platform.selectorLabels" . | nindent 8 }} + app: {{ .Values.solanaQuantBot.name }} + app.kubernetes.io/part-of: solana-quant-bot + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "{{ .Values.solanaQuantBot.metricsPort }}" + prometheus.io/path: "/metrics" + spec: + serviceAccountName: {{ .Values.solanaQuantBot.name }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + initContainers: + # Wait for database migrations to complete + - name: wait-for-migrations + image: "{{ .Values.solanaQuantBot.image.repository }}:{{ .Values.solanaQuantBot.image.tag }}" + imagePullPolicy: {{ .Values.solanaQuantBot.image.pullPolicy }} + command: ['sh', '-c', 'until python -c "import asyncio; from app.db import check_migrations; asyncio.run(check_migrations())"; do echo waiting for migrations; sleep 5; done'] + env: + - name: DB_HOST + valueFrom: + configMapKeyRef: + name: {{ include "trading-platform.fullname" . }}-config + key: POSTGRES_HOST + - name: DB_PORT + value: "5432" + - name: DB_NAME + value: {{ .Values.solanaQuantBot.env.DB_NAME | quote }} + envFrom: + - secretRef: + name: trading-secrets + resources: + {{- toYaml .Values.solanaQuantBot.resources | nindent 12 }} + containers: + - name: {{ .Values.solanaQuantBot.name }} + image: "{{ .Values.solanaQuantBot.image.repository }}:{{ .Values.solanaQuantBot.image.tag }}" + imagePullPolicy: {{ .Values.solanaQuantBot.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.solanaQuantBot.port }} + protocol: TCP + - name: metrics + containerPort: {{ .Values.solanaQuantBot.metricsPort }} + protocol: TCP + env: + {{- range $key, $value := .Values.solanaQuantBot.env }} + - name: {{ $key }} + value: {{ $value | quote }} + {{- end }} + # Shared config from ConfigMap + - name: DB_HOST + valueFrom: + configMapKeyRef: + name: {{ include "trading-platform.fullname" . }}-config + key: POSTGRES_HOST + - name: REDIS_HOST + valueFrom: + configMapKeyRef: + name: {{ include "trading-platform.fullname" . }}-config + key: REDIS_HOST + - name: KAFKA_BOOTSTRAP_SERVERS + valueFrom: + configMapKeyRef: + name: {{ include "trading-platform.fullname" . }}-config + key: KAFKA_BOOTSTRAP + - name: SOLANA_RPC_URL + valueFrom: + configMapKeyRef: + name: {{ include "trading-platform.fullname" . }}-config + key: SOLANA_RPC_URL + envFrom: + - secretRef: + name: trading-secrets + - secretRef: + name: solana-quant-bot-secrets + resources: + {{- toYaml .Values.solanaQuantBot.resources | nindent 12 }} + securityContext: + {{- toYaml .Values.containerSecurityContext | nindent 12 }} + volumeMounts: + - name: tmp + mountPath: /tmp + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 30 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + volumes: + - name: tmp + emptyDir: {} + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + nodeSelector: + kubernetes.io/os: linux + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app + operator: In + values: + - {{ .Values.solanaQuantBot.name }} + topologyKey: kubernetes.io/hostname +{{- end }} diff --git a/trading-platform/helm/trading-platform/templates/solana-quant-bot/service.yaml b/trading-platform/helm/trading-platform/templates/solana-quant-bot/service.yaml new file mode 100644 index 0000000..487514e --- /dev/null +++ b/trading-platform/helm/trading-platform/templates/solana-quant-bot/service.yaml @@ -0,0 +1,58 @@ +{{- if .Values.solanaQuantBot.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.solanaQuantBot.name }} + namespace: {{ .Values.global.namespace }} + labels: + app: {{ .Values.solanaQuantBot.name }} + app.kubernetes.io/part-of: solana-quant-bot +spec: + type: ClusterIP + ports: + - port: {{ .Values.solanaQuantBot.port }} + targetPort: http + protocol: TCP + name: http + - port: {{ .Values.solanaQuantBot.metricsPort }} + targetPort: metrics + protocol: TCP + name: metrics + selector: + app: {{ .Values.solanaQuantBot.name }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Values.solanaQuantBot.name }} + namespace: {{ .Values.global.namespace }} + labels: + app: {{ .Values.solanaQuantBot.name }} + app.kubernetes.io/part-of: solana-quant-bot + annotations: + iam.gke.io/gcp-service-account: solana-quant-bot@{{ .Values.global.clusterName }}.iam.gserviceaccount.com +--- +# Dedicated secrets for Solana quant bot (wallet keys, RPC auth) +apiVersion: v1 +kind: Secret +metadata: + name: solana-quant-bot-secrets + namespace: {{ .Values.global.namespace }} + labels: + {{- include "trading-platform.labels" . | nindent 4 }} + app.kubernetes.io/part-of: solana-quant-bot + annotations: + secrets.yaml.sops.io/v2: encrypted +type: Opaque +stringData: + # Solana wallet private key (base58 encoded) + SOLANA_BOT_PRIVATE_KEY_BASE58: {{ .Values.solanaQuantBot.env.SOLANA_BOT_PRIVATE_KEY_BASE58 | default "" | quote }} + # Helius/QuickNode API key for RPC + SOLANA_RPC_API_KEY: {{ .Values.solanaQuantBot.env.SOLANA_RPC_API_KEY | default "" | quote }} + # Jupiter API key (optional, for better rate limits) + JUPITER_API_KEY: {{ .Values.solanaQuantBot.env.JUPITER_API_KEY | default "" | quote }} + # Birdeye API key for price data + BIRDEYE_API_KEY: {{ .Values.solanaQuantBot.env.BIRDEYE_API_KEY | default "" | quote }} + # Dexscreener API key (if using authenticated endpoints) + DEXSCREENER_API_KEY: {{ .Values.solanaQuantBot.env.DEXSCREENER_API_KEY | default "" | quote }} +{{- end }} diff --git a/trading-platform/helm/trading-platform/templates/solana-rpc/service.yaml b/trading-platform/helm/trading-platform/templates/solana-rpc/service.yaml new file mode 100644 index 0000000..c2a4544 --- /dev/null +++ b/trading-platform/helm/trading-platform/templates/solana-rpc/service.yaml @@ -0,0 +1,16 @@ +{{- if .Values.solanaRpc.enabled }} +# Solana RPC proxy service (Helius/QuickNode endpoint) +# Exposes a stable internal DNS name for Solana RPC access +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.solanaRpc.name }} + namespace: {{ .Values.global.namespace }} + labels: + app: {{ .Values.solanaRpc.name }} + app.kubernetes.io/part-of: solana-quant-bot +spec: + type: ExternalName + externalName: {{ .Values.solanaRpc.externalHost }} +--- +{{- end }} diff --git a/trading-platform/helm/trading-platform/values.yaml b/trading-platform/helm/trading-platform/values.yaml index faeee27..8dcbf95 100644 --- a/trading-platform/helm/trading-platform/values.yaml +++ b/trading-platform/helm/trading-platform/values.yaml @@ -148,7 +148,7 @@ dashboard: name: dashboard image: repository: "" - tag: "0.1.0" + tag: "0.2.0-autonomous-bot" pullPolicy: IfNotPresent port: 3000 resources: @@ -166,6 +166,8 @@ dashboard: env: NODE_ENV: "production" NEXT_PUBLIC_API_BASE_URL: "/api" + NEXT_PUBLIC_EXEC_SERVICE_URL: "http://execute-service:8000" + NEXT_PUBLIC_NEWS_SERVICE_URL: "http://news-service:8001" # ================================================================= # Ingress @@ -287,6 +289,89 @@ kafka: - name: news.analysis partitions: 6 replicationFactor: 3 + # Solana quant bot topics + - name: solana.bot.signals + partitions: 6 + replicationFactor: 3 + - name: solana.bot.trades + partitions: 6 + replicationFactor: 3 + - name: solana.bot.alerts + partitions: 3 + replicationFactor: 3 + - name: solana.token.data + partitions: 6 + replicationFactor: 3 + - name: solana.pool.data + partitions: 6 + replicationFactor: 3 + +# ================================================================= +# Solana RPC (ExternalName service for external RPC providers) +# ================================================================= +solanaRpc: + enabled: true + name: solana-rpc + # ExternalName points to external RPC provider (Helius, QuickNode, etc.) + externalName: "mainnet.helius-rpc.com" + port: 443 + # env var for internal DNS: http://solana-rpc..svc.cluster.local:443 + +# ================================================================= +# Solana Quant Bot (autonomous trading agent) +# ================================================================= +solanaQuantBot: + enabled: true + name: solana-quant-bot + replicaCount: 2 + image: + repository: "" + tag: "0.1.0" + pullPolicy: IfNotPresent + port: 8000 + metricsPort: 9090 + resources: + requests: + cpu: 1 + memory: 1Gi + limits: + cpu: "2" + memory: 2Gi + migrationResources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: "500m" + memory: 512Mi + autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 5 + targetCPUUtilizationPercentage: 70 + env: + LOG_LEVEL: "INFO" + DB_NAME: "solana_quant_db" + KAFKA_GROUP_ID: "solana-quant-bot" + KAFKA_TOPIC_SIGNALS: "solana.bot.signals" + KAFKA_TOPIC_TRADES: "solana.bot.trades" + KAFKA_TOPIC_ALERTS: "solana.bot.alerts" + KAFKA_TOPIC_TOKEN_DATA: "solana.token.data" + KAFKA_TOPIC_POOL_DATA: "solana.pool.data" + # Solana RPC via internal DNS (ExternalName service) + SOLANA_RPC_ENDPOINT: "http://solana-rpc:443" + # Trading config + MIN_LIQUIDITY_USD: "50000" + MAX_POSITION_SIZE_USD: "10000" + SLIPPAGE_BPS: "100" + SCORING_THRESHOLD: "60" + HEALTHCHECK_INTERVAL: "300" + # Secrets (override via secret, not here) + SOLANA_BOT_PRIVATE_KEY_BASE58: "" + SOLANA_RPC_API_KEY: "" + BIRDEYE_API_KEY: "" + JUPITER_API_KEY: "" + DEXSCREENER_API_KEY: "" # ================================================================= # Network Policies (from security audit) @@ -297,6 +382,27 @@ networkPolicies: defaultDenyEgress: true allowDNS: true +# ================================================================= +# Observability (Prometheus + Grafana + Alertmanager) +# ================================================================= +prometheus: + enabled: true + scrapeInterval: "15s" + retention: "15d" + retentionSize: "10GB" + +alertmanager: + enabled: true + # Alert routes configured separately + +grafana: + enabled: true + namespace: "" # defaults to global.namespace + adminUser: admin + # adminPassword comes from secret + dashboards: + - solana-quant-bot-dashboard + # ================================================================= # SOPS Encryption # ================================================================= diff --git a/trading-platform/scripts/solana-funding/README.md b/trading-platform/scripts/solana-funding/README.md new file mode 100644 index 0000000..5e6f676 --- /dev/null +++ b/trading-platform/scripts/solana-funding/README.md @@ -0,0 +1,34 @@ +# Solana Quant Bot Funding + +Scripts for funding the Solana Quant Bot wallet and verifying balances. + +## Initial Funding + +```bash +# Dry run first +./fund-bot-wallet.sh --amount 50 --from-wallet --namespace customer1 --dry-run + +# Execute funding +./fund-bot-wallet.sh --amount 50 --from-wallet --namespace customer1 + +# Verify balance after +./fund-bot-wallet.sh --bot-wallet --verify +``` + +## Options + +| Option | Description | +|--------|-------------| +| `--amount SOL` | Amount in SOL to fund (default: 50) | +| `--from-wallet ADDR` | Source wallet (required for transfers) | +| `--bot-wallet ADDR` | Bot wallet (auto-read from K8s secret if omitted) | +| `--namespace NS` | K8s namespace (default: customer1) | +| `--dry-run` | Simulate without sending | +| `--verify` | Check bot wallet balance | + +## Security Notes + +- The bot wallet private key is stored as a SOPS-encrypted K8s secret. +- Initial funding should use a dedicated hot wallet, not a multi-sig for simplicity. +- sec-ops may require multi-sig for larger seed amounts. +- Monitor the bot wallet for anomalous transactions via the Prometheus alerts. diff --git a/trading-platform/scripts/solana-funding/fund-bot-wallet.sh b/trading-platform/scripts/solana-funding/fund-bot-wallet.sh new file mode 100755 index 0000000..0d45b1a --- /dev/null +++ b/trading-platform/scripts/solana-funding/fund-bot-wallet.sh @@ -0,0 +1,177 @@ +#!/usr/bin/env bash +# fund-bot-wallet.sh — Initial funding script for Solana Quant Bot +# +# Usage: ./fund-bot-wallet.sh [OPTIONS] +# +# Options: +# --amount SOL Amount in SOL to fund (default: 50) +# --from-wallet ADDR Source wallet address (required) +# --bot-wallet ADDR Bot wallet address (default: reads from K8s secret) +# --namespace NS K8s namespace (default: customer1) +# --dry-run Simulate transfer without sending +# --verify Verify current bot wallet balance (no transfer) +# --help Show this help +# +# This script: +# 1. Reads the bot's wallet address from the K8s secret +# 2. Sends SOL from --from-wallet to the bot wallet +# 3. Verifies the balance on-chain and in K8s +# +# Prerequisites: +# - solana-cli installed and configured +# - kubectl configured for the cluster +# - --from-wallet must have enough SOL for the transfer + fees + +set -euo pipefail + +# Defaults +AMOUNT="50" +FROM_WALLET="" +BOT_WALLET="" +NAMESPACE="customer1" +DRY_RUN=false +VERIFY_ONLY=false + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --amount) AMOUNT="$2"; shift 2 ;; + --from-wallet) FROM_WALLET="$2"; shift 2 ;; + --bot-wallet) BOT_WALLET="$2"; shift 2 ;; + --namespace) NAMESPACE="$2"; shift 2 ;; + --dry-run) DRY_RUN=true; shift ;; + --verify) VERIFY_ONLY=true; shift ;; + --help) + head -30 "$0" | tail -25 + exit 0 + ;; + *) echo "Unknown option: $1"; exit 1 ;; + esac +done + +# Helper: get bot wallet from K8s secret if not explicitly provided +get_bot_wallet_from_secret() { + if [[ -n "$BOT_WALLET" ]]; then + echo "$BOT_WALLET" + return + fi + + echo "Reading bot wallet from K8s secret..." + BOT_WALLET=$(kubectl -n "$NAMESPACE" get secret solana-quant-bot-secrets \ + -o jsonpath='{.data.SOLANA_BOT_WALLET_ADDRESS}' 2>/dev/null | base64 -d 2>/dev/null || true) + + if [[ -z "$BOT_WALLET" ]]; then + echo "ERROR: Could not read bot wallet from secret solana-quant-bot-secrets in namespace $NAMESPACE" + echo "Either deploy the secret first or pass --bot-wallet explicitly." + exit 1 + fi + echo "Bot wallet: $BOT_WALLET" +} + +# Verify balance +verify_balance() { + local wallet="$1" + echo "=== Verifying balance for $wallet ===" + + # On-chain balance + local on_chain_balance + on_chain_balance=$(solana balance "$wallet" --output json 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin))" 2>/dev/null || echo "0") + echo "On-chain SOL balance: $on_chain_balance" + + # Expected minimum + local expected + expected=$(echo "$AMOUNT * 0.99" | bc) # account for fees + local is_ok + is_ok=$(echo "$on_chain_balance >= $expected" | bc -l) + + if [[ "$is_ok" -eq 1 ]]; then + echo "OK: Balance >= $expected SOL (transferred amount minus fees)" + else + echo "WARNING: Balance ($on_chain_balance) < expected ($expected SOL)" + echo " The transfer may still be pending confirmation." + fi +} + +# --- Main --- + +echo "============================================" +echo " Solana Quant Bot — Initial Funding" +echo "============================================" +echo " Amount: $AMOUNT SOL" +echo " Namespace: $NAMESPACE" +echo " Dry run: $DRY_RUN" +echo "============================================" + +# Validate from-wallet +if [[ -z "$FROM_WALLET" ]]; then + echo "ERROR: --from-wallet is required (source wallet address)" + exit 1 +fi + +# Get bot wallet +get_bot_wallet_from_secret + +# Verify mode +if [[ "$VERIFY_ONLY" == true ]]; then + verify_balance "$BOT_WALLET" + exit 0 +fi + +# Dry run +if [[ "$DRY_RUN" == true ]]; then + echo "" + echo "DRY RUN — Would transfer $AMOUNT SOL from:" + echo " $FROM_WALLET -> $BOT_WALLET" + echo "" + echo "Source wallet balance:" + solana balance "$FROM_WALLET" + echo "" + echo "Bot wallet balance:" + solana balance "$BOT_WALLET" + echo "" + echo "To execute, remove --dry-run." + exit 0 +fi + +# Check source wallet has enough +echo "Checking source wallet balance..." +SOURCE_BALANCE=$(solana balance "$FROM_WALLET" --output json 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin))" 2>/dev/null || echo "0") +echo "Source wallet: $SOURCE_BALANCE SOL" + +HAS_ENOUGH=$(echo "$SOURCE_BALANCE >= $AMOUNT" | bc -l) +if [[ "$HAS_ENOUGH" -ne 1 ]]; then + echo "ERROR: Source wallet ($SOURCE_BALANCE SOL) has less than $AMOUNT SOL" + exit 1 +fi + +# Confirm before sending (unless CI) +if [[ "${CI:-false}" != "true" ]]; then + echo "" + read -p "Send $AMOUNT SOL from $FROM_WALLET to $BOT_WALLET? (yes/no): " CONFIRM + if [[ "$CONFIRM" != "yes" ]]; then + echo "Aborted." + exit 0 + fi +fi + +# Send transaction +echo "Sending transaction..." +TX_SIGNATURE=$(solana transfer "$FROM_WALLET" \ + "$BOT_WALLET" \ + "$AMOUNT" \ + --with-block-time \ + --output json 2>&1 | tee /tmp/solana-fund-tx.json) + +echo "Transaction: $TX_SIGNATURE" + +# Wait for confirmation +echo "Waiting for confirmation..." +solana confirm "$(echo "$TX_SIGNATURE" | python3 -c "import sys,json; print(json.load(sys.stdin)['signature'])")" --with-max-confirm-retries 20 + +# Verify balance +echo "" +verify_balance "$BOT_WALLET" + +echo "" +echo "Funding complete. Bot wallet: $BOT_WALLET" +echo "Transferred: $AMOUNT SOL"