Production-style user service deploy¶
Stand up canswim as user-level systemd units on a Linux host with:
| Surface | Exposure | Auth |
|---|---|---|
| Gradio dashboard | Private — Tailscale (or other VPN/LAN) only | Network isolation; optional app password via env |
| MCP | Public via reverse proxy + Funnel (or similar) | API key at the gateway (?apikey= / owner key whitelist) |
NOT FINANCIAL OR INVESTMENT ADVICE. USE AT YOUR OWN RISK.
This page is the operator guide. CLI/MCP flags: cli.md, mcp.md. Data layout: data_store.md.
Architecture (recommended)¶
Tailscale peers ──HTTP──► Gradio :7860 (dashboard; not on public Funnel)
▲
│ shared data_dir (parquet + DuckDB)
│
Internet ──Funnel──► Caddy :8080 ──apikey──► FastMCP 127.0.0.1:3472/mcp
Separate processes:
- Dashboard —
python -m canswim dashboard --same_data True(bind0.0.0.0:7860or Tailscale IP only). - MCP —
python -m canswim mcp --http --host 127.0.0.1 --port 3472(Streamable HTTP /streamable-httptransport; localhost only; gateway is the public edge).
Do not put the Gradio UI on the public Funnel. Do not expose the MCP bind port directly on the public internet without the gateway apikey check.
Prerequisites¶
- Linux user account with systemd --user (and preferably
loginctl enable-linger $USERso services survive logout). - Checkout of canswim (or
pip install canswim) and a Python env with dependencies (torch/darts for forecast, Gradio for UI,mcpfor FastMCP). CPU works. When CUDA is available, canswim uses it automatically (torch.cuda.is_available()/ Lightningaccelerator="auto"). No host-specific paths are required in application code. - PyTorch must match the machine (same as any torch app): install a wheel that supports your GPU (or CPU-only). Common cases — Ampere/Ada (e.g. RTX 3090/4090) with current CUDA 12.x wheels; very new archs may need a newer torch/CUDA build from pytorch.org. If
cuda.is_available()is true butpredictfails with no kernel image is available for execution on the device, the installed torch was built without kernels for that GPU — fix the env’s torch, not canswim flags. Point wrappers at the interpreter you verified:Environment=PYTHON=/path/to/venv/bin/python. - Shared CANSWIM_HOME (canonical:
~/.canswim/) for local config and data — not a second~/.canswim-dashboardtree:
| Path | Role |
|---|---|
~/.canswim/data/ |
Parquet (data-3rd-party/), forecast/, DuckDB |
~/.canswim/backups/ |
Optional snapshots |
~/.canswim/service/ |
User-systemd wrappers + unit templates + operator notes |
Secrets stay in ~/.env. Application code (CANSWIM_DIR checkout or site-packages) is separate from home state.
- Model checkpoint available for forecast (canswim_model.pt in the process cwd, or HF download when hfhub_sync=True). Under torch ≥2.6, Darts full-model pickles need weights_only=False; canswim applies that only around trusted checkpoint loads.
- Secrets in ~/.env (not committed): e.g. FMP_API_KEY, CANSWIM_MCP_KEY, optional HF_TOKEN / dashboard password.
- Optional public MCP: reverse proxy (Caddy recommended) and a single Tailscale Funnel (or TLS terminator) to the proxy port.
1. Layout and env¶
# Example paths — adjust to your host
export CANSWIM_DIR=~/canswim # git checkout (or wherever you install from source)
export CANSWIM_HOME=~/.canswim # canonical local config + data
export data_dir=$CANSWIM_HOME/data
export db_file=canswim_local.duckdb
export hfhub_sync=False
mkdir -p "$data_dir/data-3rd-party" "$data_dir/forecast" \
"$CANSWIM_HOME/service" "$CANSWIM_HOME/backups"
# optional: symlink checkout data/ → shared data so relative paths resolve
# ln -sfn "$data_dir" "$CANSWIM_DIR/data"
~/.env (examples — use strong random values):
FMP_API_KEY=... # market data (name may also be FMP_API_Key on some hosts)
CANSWIM_MCP_KEY=canswim_... # only key accepted by the gateway for /mcp/canswim*
# optional UI password if your Gradio build honors it:
# DASHBOARD_SECRET_KEY=...
Wrappers should map legacy names if needed:
export FMP_API_KEY="${FMP_API_KEY:-${FMP_API_Key:-}}"
2. Dashboard unit (private GUI)¶
Wrapper ~/.canswim/service/run.sh (illustrative):
#!/usr/bin/env bash
set -euo pipefail
set -a; source "$HOME/.env" 2>/dev/null || true; set +a
export FMP_API_KEY="${FMP_API_KEY:-${FMP_API_Key:-}}"
: "${CANSWIM_DIR:=$HOME/canswim}"
: "${CANSWIM_HOME:=$HOME/.canswim}"
: "${CANSWIM_DASHBOARD_HOST:=0.0.0.0}"
: "${CANSWIM_DASHBOARD_PORT:=7860}"
: "${PYTHON:=${CANSWIM_PYTHON:-$(command -v python3)}}"
cd "$CANSWIM_DIR"
export PYTHONPATH="${CANSWIM_DIR}/src${PYTHONPATH:+:$PYTHONPATH}"
export GRADIO_SERVER_NAME="$CANSWIM_DASHBOARD_HOST"
export GRADIO_SERVER_PORT="$CANSWIM_DASHBOARD_PORT"
export data_dir="${CANSWIM_HOME}/data"
export db_file=canswim_local.duckdb
export hfhub_sync=False
export OMP_NUM_THREADS=2 TORCH_NUM_THREADS=2
exec env data-3rd-party=data-3rd-party "$PYTHON" -m canswim dashboard --same_data True
Unit ~/.config/systemd/user/canswim-dashboard.service (template also kept under ~/.canswim/service/):
[Unit]
Description=CANSWIM Gradio Dashboard (Tailscale UI)
After=network-online.target
[Service]
Type=simple
WorkingDirectory=%h/canswim
EnvironmentFile=-%h/.env
Environment=CANSWIM_HOME=%h/.canswim
Environment=CANSWIM_DASHBOARD_HOST=0.0.0.0
Environment=CANSWIM_DASHBOARD_PORT=7860
ExecStart=%h/.canswim/service/run.sh
Restart=always
RestartSec=10
MemoryMax=4G
CPUQuota=150%
Nice=10
[Install]
WantedBy=default.target
chmod +x ~/.canswim/service/run.sh
cp ~/.canswim/service/canswim-dashboard.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now canswim-dashboard
systemctl --user status canswim-dashboard --no-pager
Access (private):
http://<tailscale-ip>:7860/
http://<magicdns-name>:7860/
Confirm the UI is not listed on public Funnel routes. Prefer binding only the Tailscale IP if the host also has a public interface.
3. MCP unit (localhost Streamable HTTP)¶
python -m canswim mcp --http --host 127.0.0.1 --port 3472
Wrapper ~/.canswim/service/run-mcp.sh (illustrative):
#!/usr/bin/env bash
set -euo pipefail
set -a; source "$HOME/.env" 2>/dev/null || true; set +a
export FMP_API_KEY="${FMP_API_KEY:-${FMP_API_Key:-}}"
: "${CANSWIM_DIR:=$HOME/canswim}"
: "${CANSWIM_HOME:=$HOME/.canswim}"
: "${CANSWIM_MCP_HOST:=127.0.0.1}"
: "${CANSWIM_MCP_PORT:=3472}"
: "${PYTHON:=${CANSWIM_PYTHON:-$(command -v python3)}}"
cd "$CANSWIM_DIR"
export PYTHONPATH="${CANSWIM_DIR}/src${PYTHONPATH:+:$PYTHONPATH}"
export data_dir="${CANSWIM_HOME}/data"
export db_file=canswim_local.duckdb
export hfhub_sync=False
# Leave MCP_ALLOW_RUNS unset for read-only public tools
exec env data-3rd-party=data-3rd-party "$PYTHON" -m canswim mcp \
--http --host "$CANSWIM_MCP_HOST" --port "$CANSWIM_MCP_PORT"
Unit ~/.config/systemd/user/canswim-mcp.service:
[Unit]
Description=CANSWIM FastMCP (Streamable HTTP for gateway)
After=network-online.target
[Service]
Type=simple
WorkingDirectory=%h/canswim
EnvironmentFile=-%h/.env
Environment=CANSWIM_MCP_HOST=127.0.0.1
Environment=CANSWIM_MCP_PORT=3472
ExecStart=%h/.canswim/service/run-mcp.sh
Restart=always
RestartSec=5
MemoryMax=2G
Nice=10
[Install]
WantedBy=default.target
systemctl --user enable --now canswim-mcp
curl -sS -X POST "http://127.0.0.1:3472/mcp" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"local","version":"1"}}}'
FastMCP serves the protocol at /mcp on that port. Details: mcp.md.
3b. Weekend job (in-process — no extra systemd unit)¶
Recurring all-DB gather + monthly catch-up (~12 origins) + live week
forecast for every DuckDB stock_tickers symbol runs inside the long-lived
canswim process via APScheduler
(BackgroundScheduler + CronTrigger). Do not add a separate timer service.
| Process | Default |
|---|---|
MCP with MCP_ALLOW_RUNS=1 |
Scheduler on (Sat 06:00 local) |
| Dashboard | Scheduler off unless CANSWIM_WEEKEND_SCHEDULER=1 |
| Concurrent heavy work | Idempotent job units first: weekend catch-up = same refresh job registry as MCP; clients coalesce. Narrow fcntl.flock only guards parquet write batches vs CLI (kernel-released on crash/reboot) |
| Multi-client refresh | Same/subset ticker list while a job runs (including weekend source=weekend) → coalesce to one job_id |
# MCP unit already has MCP_ALLOW_RUNS=1 on this host → weekend cron is active after restart
systemctl --user restart canswim-mcp
# Confirm: get_server_info → weekend_scheduler.running true; next_run_time set
# Force on/off
Environment=CANSWIM_WEEKEND_SCHEDULER=1 # or 0
# Cron (local TZ): day-of-week, hour, minute
Environment=CANSWIM_WEEKEND_DOW=sat
Environment=CANSWIM_WEEKEND_HOUR=6
Environment=CANSWIM_WEEKEND_MINUTE=0
# Catch-up is ON by default (monthly backtests + live). Live-only:
# Environment=CANSWIM_WEEKEND_CATCHUP=0
Manual one-shot (same code path, no schedule):
data_dir=$HOME/.canswim/data python -m canswim weekend --dry_run
data_dir=$HOME/.canswim/data python -m canswim weekend
# or: ./weekend.sh --dry_run
Legacy service/canswim-weekend.timer templates are not recommended (extra unit to operate). Prefer in-process scheduling.
4. Public MCP via gateway + apikey¶
Use one edge (e.g. Tailscale Funnel → Caddy :8080) for all MCPs. Pattern:
- Public path prefix:
/mcp/canswim* - Require
?apikey=equal toCANSWIM_MCP_KEYfrom the host env (owner whitelist). - On success: strip prefix and
reverse_proxy 127.0.0.1:3472. - Missing key → 401; wrong key → 403.
Connector URL shape:
https://<funnel-host>/mcp/canswim/mcp?apikey=<CANSWIM_MCP_KEY>
Auth matrix smoke:
source ~/.env
BASE="https://<funnel-host>/mcp/canswim/mcp"
# no key → 401
curl -sS -o /dev/null -w '%{http_code}\n' -X POST "$BASE" \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}'
# wrong key → 403
curl -sS -o /dev/null -w '%{http_code}\n' -X POST "$BASE?apikey=wrong" \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}'
# correct key → 200
curl -sS -o /dev/null -w '%{http_code}\n' -X POST "$BASE?apikey=$CANSWIM_MCP_KEY" \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}'
Keep MCP_ALLOW_RUNS unset on the public MCP process unless you intentionally allow remote gather/forecast (heavy; prefer CLI on the host for data population).
5. Data population (after services exist)¶
cd "$CANSWIM_DIR"
export PYTHONPATH="$CANSWIM_DIR/src" data_dir="$CANSWIM_HOME/data" hfhub_sync=False
# example: checked-in IBD50 list
TICKERS=$(tail -n +2 symbol_lists/IBD50.csv | paste -sd, -)
python -m canswim gatherdata --tickers "$TICKERS"
python -m canswim forecast --tickers "$TICKERS" # or a smaller subset first
# rebuild search DB so Charts/MCP see new symbols (dashboard --same_data False once, or Run-tab rebuild)
Search DB semantics: data_store.md. Forecast needs complete covariates (industry funds / broad market); yfinance must not use a custom session that breaks Yahoo (default canswim leaves session to yfinance).
6. Operations¶
systemctl --user restart canswim-dashboard canswim-mcp
systemctl --user status canswim-dashboard canswim-mcp --no-pager
journalctl --user -u canswim-dashboard -u canswim-mcp --since "5 min ago" -q
Resource limits (MemoryMax / CPUQuota / thread env) are strongly recommended on shared hosts.
Security checklist¶
- [ ] Dashboard not on public Funnel / open internet without VPN.
- [ ] MCP process binds 127.0.0.1 only.
- [ ] Gateway enforces owner
CANSWIM_MCP_KEY(not an openapikey=*alone). - [ ] Secrets only in
~/.env(mode 600); never in git. - [ ] Public MCP stays read-only unless you deliberately set
MCP_ALLOW_RUNS=1. - [ ] Unit
ReadWritePathslimited to data dirs when using hardening options.
Related¶
- mcp.md — tools and HTTP flags
- cli.md — tasks and env
- data_store.md — parquet vs DuckDB
- run_triggers.md — gather/forecast policy