Files
foxhunt/scripts/deploy_broker_gateway.sh
jgrusewski 00ef9e2866 Wave 15: Complete FactoredAction migration to 45-action system
Major Changes:
- Migrated from 3-action TradingAction to 45-action FactoredAction
- 45 actions: 5 exposure × 3 order types × 3 urgency levels
- Absolute exposure model (target positions -1.0 to +1.0)
- Transaction cost differentiation (Market 0.15%, LimitMaker 0.05%, IoC 0.10%)
- Fixed action diversity threshold (1.11% → 0.5% for 45-action space)

Bug Fixes:
- Bug #15: Incomplete FactoredAction integration (code existed but unused)
- Bug #16: Runtime crash in action diversity checking (hardcoded 3-action match)

Code Changes (13 files, ~464 lines):
- ml/src/dqn/action_space.rs: Core FactoredAction + 4 helper methods
- ml/src/trainers/dqn.rs: Action diversity refactored (3→45 dynamic)
- ml/src/dqn/reward.rs: calculate_reward() signature updated
- ml/src/dqn/portfolio_tracker.rs: execute_action() absolute exposure
- ml/src/dqn/dqn.rs: WorkingDQN action selection migrated
- ml/tests/*.rs: 9 test files updated with FactoredAction assertions

Test Results:
- 1-epoch smoke test: 100% action diversity (45/45 actions, 80.2s)
- 10-epoch production: 87.8% readiness (79/90 scorecard, 14.0 min)
- Loss convergence: 96.9% reduction (119K → 3.6K)
- Action diversity: 100% → 44% (healthy specialization)
- Checkpoint reliability: 12/12 files saved (100%)
- DQN tests: 195/195 passing (100%)
- ML baseline: 1,514/1,515 passing (99.93%)

Production Status:  CERTIFIED (87.8% readiness)
Go/No-Go:  GO FOR 100-EPOCH PRODUCTION TRAINING

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 23:27:02 +01:00

458 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
# Foxhunt Broker Gateway Service - Production Deployment Script
# Builds multi-arch Docker image, pushes to registry, deploys to Kubernetes
# Includes smoke tests and automatic rollback on failure
set -euo pipefail
# ============================================================================
# Configuration
# ============================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
SERVICE_DIR="${PROJECT_ROOT}/services/broker_gateway_service"
K8S_DIR="${SERVICE_DIR}/k8s"
# Docker configuration
DOCKER_REGISTRY="${DOCKER_REGISTRY:-jgrusewski}"
IMAGE_NAME="${IMAGE_NAME:-foxhunt-broker-gateway}"
IMAGE_TAG="${IMAGE_TAG:-latest}"
FULL_IMAGE_NAME="${DOCKER_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}"
# Kubernetes configuration
NAMESPACE="${NAMESPACE:-foxhunt}"
DEPLOYMENT_NAME="broker-gateway-service"
TIMEOUT="${TIMEOUT:-300}" # 5 minutes
# Build configuration
BUILD_PLATFORMS="${BUILD_PLATFORMS:-linux/amd64,linux/arm64}"
SKIP_BUILD="${SKIP_BUILD:-false}"
SKIP_PUSH="${SKIP_PUSH:-false}"
SKIP_TESTS="${SKIP_TESTS:-false}"
DRY_RUN="${DRY_RUN:-false}"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# ============================================================================
# Helper Functions
# ============================================================================
log_info() {
echo -e "${BLUE}[INFO]${NC} $*"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $*"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $*"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $*"
}
# Check if command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Check prerequisites
check_prerequisites() {
log_info "Checking prerequisites..."
local missing_tools=()
if ! command_exists docker; then
missing_tools+=("docker")
fi
if ! command_exists kubectl; then
missing_tools+=("kubectl")
fi
if [[ "$SKIP_BUILD" == "false" ]]; then
if ! docker buildx version >/dev/null 2>&1; then
log_error "Docker BuildKit is not available. Install with: docker buildx create --use"
exit 1
fi
fi
if [[ ${#missing_tools[@]} -gt 0 ]]; then
log_error "Missing required tools: ${missing_tools[*]}"
log_error "Please install missing tools and try again"
exit 1
fi
log_success "All prerequisites are installed"
}
# Get git commit hash
get_git_commit() {
if command_exists git && git rev-parse --git-dir >/dev/null 2>&1; then
git rev-parse --short HEAD
else
echo "unknown"
fi
}
# Get build date
get_build_date() {
date -u +"%Y-%m-%dT%H:%M:%SZ"
}
# Build Docker image
build_docker_image() {
if [[ "$SKIP_BUILD" == "true" ]]; then
log_warn "Skipping Docker build (SKIP_BUILD=true)"
return 0
fi
log_info "Building Docker image: ${FULL_IMAGE_NAME}"
log_info "Platforms: ${BUILD_PLATFORMS}"
local git_commit
git_commit=$(get_git_commit)
local build_date
build_date=$(get_build_date)
cd "${PROJECT_ROOT}"
# Build multi-arch image with BuildKit
docker buildx build \
--platform "${BUILD_PLATFORMS}" \
--file "${SERVICE_DIR}/Dockerfile.production" \
--tag "${FULL_IMAGE_NAME}" \
--tag "${DOCKER_REGISTRY}/${IMAGE_NAME}:${git_commit}" \
--build-arg "GIT_COMMIT=${git_commit}" \
--build-arg "BUILD_DATE=${build_date}" \
--build-arg "VERSION=${IMAGE_TAG}" \
${SKIP_PUSH:+--load} \
${SKIP_PUSH:---push} \
.
if [[ "$SKIP_PUSH" == "true" ]]; then
log_success "Docker image built locally (not pushed)"
else
log_success "Docker image built and pushed: ${FULL_IMAGE_NAME}"
fi
}
# Check Docker image size
check_image_size() {
if [[ "$SKIP_BUILD" == "true" ]]; then
return 0
fi
log_info "Checking image size..."
local image_size
image_size=$(docker image inspect "${FULL_IMAGE_NAME}" --format='{{.Size}}' 2>/dev/null || echo "0")
if [[ "$image_size" -eq 0 ]]; then
log_warn "Could not determine image size (image may not be loaded locally)"
return 0
fi
local size_mb=$((image_size / 1024 / 1024))
log_info "Image size: ${size_mb}MB"
# Warn if image is larger than expected (>100MB)
if [[ $size_mb -gt 100 ]]; then
log_warn "Image size is larger than expected (${size_mb}MB > 100MB)"
log_warn "Consider optimizing the image"
else
log_success "Image size is optimal (${size_mb}MB)"
fi
}
# Check Kubernetes connectivity
check_k8s_connectivity() {
log_info "Checking Kubernetes connectivity..."
if ! kubectl cluster-info >/dev/null 2>&1; then
log_error "Cannot connect to Kubernetes cluster"
log_error "Check your kubeconfig and cluster status"
exit 1
fi
log_success "Connected to Kubernetes cluster"
# Check if namespace exists, create if not
if ! kubectl get namespace "${NAMESPACE}" >/dev/null 2>&1; then
log_warn "Namespace '${NAMESPACE}' does not exist"
if [[ "$DRY_RUN" == "false" ]]; then
log_info "Creating namespace '${NAMESPACE}'..."
kubectl create namespace "${NAMESPACE}"
log_success "Namespace created"
else
log_info "[DRY-RUN] Would create namespace '${NAMESPACE}'"
fi
fi
}
# Apply Kubernetes manifests
apply_k8s_manifests() {
log_info "Applying Kubernetes manifests..."
local manifests=(
"configmap.yaml"
"secret.yaml"
"service.yaml"
"deployment.yaml"
"hpa.yaml"
)
for manifest in "${manifests[@]}"; do
local manifest_path="${K8S_DIR}/${manifest}"
if [[ ! -f "$manifest_path" ]]; then
log_error "Manifest not found: ${manifest_path}"
exit 1
fi
log_info "Applying ${manifest}..."
if [[ "$DRY_RUN" == "true" ]]; then
kubectl apply -f "${manifest_path}" --namespace="${NAMESPACE}" --dry-run=client
log_info "[DRY-RUN] Would apply ${manifest}"
else
kubectl apply -f "${manifest_path}" --namespace="${NAMESPACE}"
log_success "Applied ${manifest}"
fi
done
log_success "All manifests applied"
}
# Wait for rollout to complete
wait_for_rollout() {
if [[ "$DRY_RUN" == "true" ]]; then
log_info "[DRY-RUN] Would wait for rollout to complete"
return 0
fi
log_info "Waiting for rollout to complete (timeout: ${TIMEOUT}s)..."
if kubectl rollout status statefulset/"${DEPLOYMENT_NAME}" \
--namespace="${NAMESPACE}" \
--timeout="${TIMEOUT}s"; then
log_success "Rollout completed successfully"
return 0
else
log_error "Rollout failed or timed out"
return 1
fi
}
# Get pod status
get_pod_status() {
kubectl get pods \
--namespace="${NAMESPACE}" \
--selector="app=${DEPLOYMENT_NAME}" \
--output=json |
jq -r '.items[] | "\(.metadata.name): \(.status.phase) (Ready: \(.status.conditions[] | select(.type=="Ready") | .status))"'
}
# Run smoke tests
run_smoke_tests() {
if [[ "$SKIP_TESTS" == "true" ]]; then
log_warn "Skipping smoke tests (SKIP_TESTS=true)"
return 0
fi
if [[ "$DRY_RUN" == "true" ]]; then
log_info "[DRY-RUN] Would run smoke tests"
return 0
fi
log_info "Running smoke tests..."
# Get pod name
local pod_name
pod_name=$(kubectl get pods \
--namespace="${NAMESPACE}" \
--selector="app=${DEPLOYMENT_NAME}" \
--output=jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "")
if [[ -z "$pod_name" ]]; then
log_error "No pods found for deployment ${DEPLOYMENT_NAME}"
return 1
fi
log_info "Testing pod: ${pod_name}"
# Test 1: Health check endpoint
log_info "Test 1: Health check endpoint..."
if kubectl exec "${pod_name}" --namespace="${NAMESPACE}" -- \
curl -sf http://localhost:8086/health >/dev/null; then
log_success "Health check passed"
else
log_error "Health check failed"
return 1
fi
# Test 2: gRPC health probe
log_info "Test 2: gRPC health probe..."
if kubectl exec "${pod_name}" --namespace="${NAMESPACE}" -- \
/usr/local/bin/grpc_health_probe -addr=localhost:50056; then
log_success "gRPC health probe passed"
else
log_error "gRPC health probe failed"
return 1
fi
# Test 3: Metrics endpoint
log_info "Test 3: Metrics endpoint..."
if kubectl exec "${pod_name}" --namespace="${NAMESPACE}" -- \
curl -sf http://localhost:9096/metrics >/dev/null; then
log_success "Metrics endpoint passed"
else
log_warn "Metrics endpoint failed (non-critical)"
fi
log_success "All smoke tests passed"
return 0
}
# Rollback deployment
rollback_deployment() {
log_error "Deployment failed. Rolling back..."
if [[ "$DRY_RUN" == "true" ]]; then
log_info "[DRY-RUN] Would rollback deployment"
return 0
fi
kubectl rollout undo statefulset/"${DEPLOYMENT_NAME}" --namespace="${NAMESPACE}"
log_info "Waiting for rollback to complete..."
kubectl rollout status statefulset/"${DEPLOYMENT_NAME}" \
--namespace="${NAMESPACE}" \
--timeout="${TIMEOUT}s"
log_warn "Rollback completed"
}
# Print deployment summary
print_summary() {
log_info "==================================================================="
log_info "Deployment Summary"
log_info "==================================================================="
log_info "Image: ${FULL_IMAGE_NAME}"
log_info "Namespace: ${NAMESPACE}"
log_info "Deployment: ${DEPLOYMENT_NAME}"
log_info "-------------------------------------------------------------------"
log_info "Pod Status:"
get_pod_status
log_info "==================================================================="
}
# ============================================================================
# Main Deployment Flow
# ============================================================================
main() {
log_info "Starting Broker Gateway Service deployment..."
log_info "Image: ${FULL_IMAGE_NAME}"
log_info "Namespace: ${NAMESPACE}"
log_info "Dry-run: ${DRY_RUN}"
# Step 1: Check prerequisites
check_prerequisites
# Step 2: Build Docker image
build_docker_image
# Step 3: Check image size
check_image_size
# Step 4: Check Kubernetes connectivity
check_k8s_connectivity
# Step 5: Apply Kubernetes manifests
apply_k8s_manifests
# Step 6: Wait for rollout to complete
if ! wait_for_rollout; then
rollback_deployment
exit 1
fi
# Step 7: Run smoke tests
if ! run_smoke_tests; then
log_error "Smoke tests failed"
rollback_deployment
exit 1
fi
# Step 8: Print deployment summary
print_summary
log_success "Broker Gateway Service deployment completed successfully!"
}
# ============================================================================
# Script Entry Point
# ============================================================================
# Parse command-line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--skip-build)
SKIP_BUILD=true
shift
;;
--skip-push)
SKIP_PUSH=true
shift
;;
--skip-tests)
SKIP_TESTS=true
shift
;;
--dry-run)
DRY_RUN=true
shift
;;
--namespace)
NAMESPACE="$2"
shift 2
;;
--image-tag)
IMAGE_TAG="$2"
shift 2
;;
--timeout)
TIMEOUT="$2"
shift 2
;;
--help)
echo "Usage: $0 [options]"
echo ""
echo "Options:"
echo " --skip-build Skip Docker image build"
echo " --skip-push Build locally but do not push to registry"
echo " --skip-tests Skip smoke tests"
echo " --dry-run Show what would be done without making changes"
echo " --namespace NAME Kubernetes namespace (default: foxhunt)"
echo " --image-tag TAG Docker image tag (default: latest)"
echo " --timeout SEC Rollout timeout in seconds (default: 300)"
echo " --help Show this help message"
exit 0
;;
*)
log_error "Unknown option: $1"
log_error "Use --help for usage information"
exit 1
;;
esac
done
# Run main deployment flow
main