#!/usr/bin/env bash # ============================================================================ # Charles — server installer # For a fresh Ubuntu 22.04 / 24.04 server (e.g. a DigitalOcean droplet), as root: # curl -fsSL https://charles-bot.xyz/install.sh -o install.sh && bash install.sh # Safe to re-run: finished steps are skipped, the code is updated with git pull. # ============================================================================ set -euo pipefail REPO="https://github.com/nswansonprojects/charles.git" BOT_DIR="$HOME/charles" CONDA_DIR="$HOME/miniconda3" ENV_NAME="charles" PY="$CONDA_DIR/envs/$ENV_NAME/bin/python" green='\033[0;32m'; yellow='\033[1;33m'; blue='\033[0;34m'; red='\033[0;31m'; nc='\033[0m' step() { echo -e "\n${blue}▶ $1${nc}"; } ok() { echo -e "${green}✓${nc} $1"; } note() { echo -e "${yellow}!${nc} $1"; } die() { echo -e "${red}✗ $1${nc}"; exit 1; } [ "$(id -u)" -eq 0 ] || die "Run this as root (on a fresh droplet you log in as root)." command -v apt-get >/dev/null || die "This installer supports Ubuntu/Debian only." step "1/8 System packages" export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y -qq git curl ufw tmux nano >/dev/null ok "git, curl, ufw, tmux, nano" step "2/8 Swap space (keeps small servers from running out of memory)" if swapon --show | grep -q .; then ok "swap already on" else fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap -q /swapfile && swapon /swapfile grep -q '^/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab ok "2 GB swap added" fi step "3/8 Python 3.11 (Miniconda)" if [ ! -x "$CONDA_DIR/bin/conda" ]; then curl -fsSL https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -o /tmp/miniconda.sh bash /tmp/miniconda.sh -b -p "$CONDA_DIR" >/dev/null rm -f /tmp/miniconda.sh fi if [ ! -x "$PY" ]; then # conda-forge only: no Anaconda terms-of-service prompt "$CONDA_DIR/bin/conda" create -y -q -n "$ENV_NAME" -c conda-forge --override-channels python=3.11 >/dev/null fi ok "$("$PY" --version)" step "4/8 Charles code from GitHub" if [ -d "$BOT_DIR/.git" ]; then git -C "$BOT_DIR" pull --ff-only || note "git pull failed (local changes?) — keeping your current code" elif [ -e "$BOT_DIR" ] && [ -n "$(ls -A "$BOT_DIR" 2>/dev/null)" ]; then die "$BOT_DIR exists and isn't a git checkout. Move it aside and re-run." else git clone -q "$REPO" "$BOT_DIR" fi mkdir -p "$BOT_DIR/data" "$BOT_DIR/logs" "$BOT_DIR/reports" ok "code in $BOT_DIR" step "5/8 Python packages (2–5 minutes)" "$PY" -m pip install -q --upgrade pip "$PY" -m pip install -q -r "$BOT_DIR/requirements.txt" ok "packages installed" step "6/8 Keys file, firewall, service" if [ ! -f "$BOT_DIR/.env" ]; then cp "$BOT_DIR/.env.example" "$BOT_DIR/.env" note "created $BOT_DIR/.env — add your API keys to it next" fi chmod 600 "$BOT_DIR/.env" ufw allow OpenSSH >/dev/null ufw allow 8502/tcp >/dev/null # dashboard ufw delete allow 8501/tcp >/dev/null 2>&1 || true # old Streamlit dashboard (pre-Oct 2026) ufw --force enable >/dev/null ok "firewall: SSH, 8502 (dashboard)" cat > /etc/systemd/system/charles.service < /etc/systemd/system/charles-web.service < /etc/systemd/system/charles-evaluate.service < /etc/systemd/system/charles-evaluate.timer </dev/null "$KPY" -m pip install -q -r "$KALSHI_DIR/requirements.txt" mkdir -p "$KALSHI_DIR/data" cat > /etc/systemd/system/kalshi-net.service < /etc/systemd/system/kalshi-net.timer < /etc/systemd/system/kalshi-web.service < "/usr/local/bin/$1"; chmod +x "/usr/local/bin/$1"; } ip_cmd='$(curl -s --max-time 3 ifconfig.me || hostname -I | cut -d" " -f1)' mk charles-start "systemctl start charles charles-web && echo \"✅ Charles started — dashboard: http://$ip_cmd:8502\"" mk charles-stop 'systemctl stop charles && echo "🛑 Charles stopped"' mk charles-restart 'systemctl restart charles && echo "🔄 Charles restarted"' mk charles-status "systemctl --no-pager status charles | head -5; echo; echo \"📊 Dashboard: http://$ip_cmd:8502\"" mk charles-logs "tail -n 50 -f $BOT_DIR/logs/bot.log" mk charles-keys "exec $PY $BOT_DIR/update_keys.py" mk charles-update "set -e; git -C $BOT_DIR pull --ff-only; $PY -m pip install -q -r $BOT_DIR/requirements.txt; [ -x $CONDA_DIR/envs/kalshi/bin/python ] && $CONDA_DIR/envs/kalshi/bin/python -m pip install -q -r $BOT_DIR/kalshi/requirements.txt && systemctl restart kalshi-web; systemctl restart charles charles-web; echo \"⬆️ Charles updated and restarted\"" mk kalshi-logs "journalctl -u kalshi-net -n 50 -f" ok "charles-start · stop · restart · status · logs · keys · update · kalshi-logs" IP=$(curl -s --max-time 3 ifconfig.me || hostname -I | cut -d' ' -f1) echo -e "\n${green}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${nc}" echo -e "${green} Charles is installed.${nc}" echo -e "${green}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${nc}" echo " 1. Add your keys: nano $BOT_DIR/.env (to change them later: charles-keys)" echo " 2. Start the bot: charles-start" echo " 3. Dashboard: http://$IP:8502 (Stocks | Kalshi switch at the top)" echo " Logs: charles-logs · Guide: https://charles-bot.xyz/setup.html"