Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
# The image only needs the Dockerfile; the repo is mounted at runtime.
# The image carries the code; docker-compose mounts user_data/ from the host.
*
!Dockerfile
!panel
!ridgeline
!scripts
!user_data
user_data/config.private.json
user_data/data
user_data/paper
user_data/replays
user_data/logs
user_data/audit
user_data/backtest_results
user_data/hyperopt_results
user_data/research/runs
user_data/research/bias
user_data/plot
user_data/risk_state.json
**/*.sqlite*
**/__pycache__
5 changes: 5 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Windows scripts need CRLF line endings to run reliably from cmd/PowerShell.
*.cmd text eol=crlf
*.bat text eol=crlf
*.ps1 text eol=crlf
*.iss text eol=crlf
117 changes: 117 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
name: Release

# Pushing a tag like v0.1.0 builds and smoke-tests the Windows installer and
# the Docker image, publishes the image to ghcr.io/devam2001/ridgeline and
# creates the GitHub release with the installer attached.
# Pull requests that touch packaging build and test both without publishing.

on:
push:
tags: ["v*"]
pull_request:
paths:
- "installer/**"
- "Dockerfile"
- ".dockerignore"
- "docker-compose.yml"
- "requirements.txt"
- ".github/workflows/release.yml"

permissions:
contents: read

env:
IMAGE: ghcr.io/devam2001/ridgeline

jobs:
windows-installer:
runs-on: windows-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Version
shell: pwsh
run: |
$v = (Select-String -Path pyproject.toml -Pattern '^version = "(.+)"').Matches[0].Groups[1].Value
if ($env:GITHUB_REF_TYPE -eq "tag" -and $env:GITHUB_REF_NAME -ne "v$v") {
throw "tag $env:GITHUB_REF_NAME doesn't match pyproject.toml version $v"
}
"VERSION=$v" >> $env:GITHUB_ENV

- name: Inno Setup
shell: pwsh
run: |
$iscc = Join-Path ${env:ProgramFiles(x86)} "Inno Setup 6\ISCC.exe"
if (-not (Get-Command iscc -ErrorAction SilentlyContinue) -and -not (Test-Path $iscc)) {
choco install innosetup --no-progress -y
}

- name: Build installer
shell: pwsh
run: ./installer/build.ps1 -Python (Split-Path (Get-Command python).Source) -Version $env:VERSION

- name: Smoke-test installer
shell: pwsh
run: ./installer/smoke-test.ps1 -Installer "build/installer/Ridgeline-Setup-$env:VERSION.exe"

- uses: actions/upload-artifact@v4
with:
name: windows-installer
path: build/installer/*.exe
if-no-files-found: error

docker:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4

- name: Version
run: echo "VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' pyproject.toml)" >> "$GITHUB_ENV"

- name: Build
run: docker build --build-arg VERSION="$VERSION" -t "$IMAGE:$VERSION" -t "$IMAGE:latest" .

- name: Smoke-test image
run: |
docker run --rm "$IMAGE:$VERSION" sh -c '
python scripts/init_private_config.py > /dev/null &&
freqtrade list-strategies --userdir user_data -c user_data/config.json &&
python -c "import panel.app; print(\"panel imports ok\")"'

- name: Publish
if: github.ref_type == 'tag'
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
docker push "$IMAGE:$VERSION"
docker push "$IMAGE:latest"

release:
if: github.ref_type == 'tag'
needs: [windows-installer, docker]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4

- uses: actions/download-artifact@v4
with:
name: windows-installer
path: dist

- name: Create release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "$GITHUB_REF_NAME" dist/*.exe "Crypto Trading Bot Case Study.pdf" \
--title "Ridgeline $GITHUB_REF_NAME" \
--notes-file "docs/releases/$GITHUB_REF_NAME.md"
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@ all_usdt.json
reports/
user_data/audit/
user_data/risk_state.json
*.reconcile.json
.pytest_cache/
.ruff_cache/
build/
Binary file modified Crypto Trading Bot Case Study.pdf
Binary file not shown.
22 changes: 18 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
# ridgeline runtime: the official Freqtrade image plus the few extras the
# panel and watchdog use. The repository itself is mounted at /ridgeline by
# docker-compose, so data, databases, logs and the audit trail live on the host.
# ridgeline runtime: the official Freqtrade image, the few extras the panel and
# watchdog use, and the ridgeline code itself. Releases publish it as
# ghcr.io/devam2001/ridgeline, so a server can pull it instead of building.
# docker-compose mounts user_data/ from the host: config, strategies, data,
# databases, logs and the audit trail stay there.
FROM freqtradeorg/freqtrade:2026.8

ARG VERSION=dev
LABEL org.opencontainers.image.title="ridgeline" \
org.opencontainers.image.description="Freqtrade bot with paper trading, research gates, risk controls and reporting" \
org.opencontainers.image.source="https://github.com/devam2001/ridgeline" \
org.opencontainers.image.version="${VERSION}"

# joblib 1.6 dropped a module Freqtrade's hyperopt imports
RUN pip install --no-cache-dir "joblib<1.6" httpx psutil
RUN pip install --no-cache-dir "joblib<1.6" "httpx>=0.27" "psutil>=5.9"

WORKDIR /ridgeline
COPY panel/ panel/
COPY ridgeline/ ridgeline/
COPY scripts/ scripts/
# Defaults for a container started without the user_data mount.
COPY --chown=ftuser:ftuser user_data/ user_data/

ENTRYPOINT []
71 changes: 64 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,29 +25,53 @@ panel cannot start a live bot at all.
| `user_data/strategies/TrendRider4h.py` | 4h Donchian breakout trend-follower, BTC bear-market filter, ATR chandelier exit |
| `user_data/strategies/TrendDipBuyer1h.py` | 1h buy-the-dip inside an uptrend (mean reversion) |
| `user_data/strategies/EmaRsiMomentum.py` | original 1h EMA-crossover + RSI strategy (hyperoptable) |
| `user_data/strategies/MomentumRotation1d.py` | daily rotation into the 3 strongest coins by risk-adjusted momentum while BTC is in an uptrend (candidate) |
| `user_data/config.json` | exchange, pairs, risk settings - **no secrets** |
| `user_data/config.private.json` | API login, JWT secret, exchange keys - **never share/commit** (loaded automatically via `add_config_files`) |
| `panel/` | control panel (FastAPI + plain JS) |
| `scripts/research.py` | IS/OOS backtests, walk-forward, fee stress, bias checks, go-live gates |
| `ridgeline/` | shared package: risk limits (`risk.py`, `freqtrade_risk.py`), audit trail, tear-sheet reports, alerts |
| `ridgeline/` | shared package: risk limits (`risk.py`, `freqtrade_risk.py`), audit trail, tear-sheet reports, paper-vs-replay reconciliation, alerts |
| `scripts/report.py`, `scripts/watchdog.py` | tear-sheet generator; health/risk watchdog with Telegram alerts |
| `scripts/reconcile.py` | the last go-live gate: compares a paper session with a replay of the same weeks |
| `scripts/update_data.py` | downloads/refreshes market data and the live pair list (monthly) |
| `scripts/case_study.py` | builds the research-format case study PDF from the latest results |
| `installer/`, `.github/workflows/release.yml` | Windows installer and the release pipeline (installer + Docker image) |
| `tests/`, `.github/workflows/ci.yml` | test suite (risk, audit, lookahead, universe, panel, hygiene) run by GitHub Actions |
| `Dockerfile`, `docker-compose.yml` | 24/7 deployment: bot + monitor-mode panel + watchdog |
| `docs/OPERATIONS.md` | runbook: risk limits, kill switch, alerts, VPS deployment, incidents, monthly routine |
| `RESEARCH.md` | latest research results and what they mean |

## 1. Install

### Windows installer (easiest)

Download `Ridgeline-Setup-<version>.exe` from the
[latest release](https://github.com/devam2001/ridgeline/releases/latest) and run it.
- It installs for your user only, so no admin rights are needed.
- It brings its own Python, Freqtrade and FreqUI.
- It adds **Ridgeline** (the panel) and **Update market data** to the Start menu.
- The installer isn't code-signed, so Windows SmartScreen may warn the first
time. Choose **More info → Run anyway**.
- Your `user_data` (private config, paper sessions, data, audit trail) is kept
when you upgrade or uninstall.

### Docker (servers)

A server can pull the released image instead of building it. See
[docs/OPERATIONS.md](docs/OPERATIONS.md#deploying-on-a-vps-docker).

### From source

```bash
python -m venv .venv
.venv\Scripts\activate # macOS/Linux: source .venv/bin/activate
pip install "freqtrade[hyperopt]" "joblib<1.6"
pip install -r requirements.txt
freqtrade install-ui # downloads FreqUI
freqtrade list-strategies --userdir user_data # all three should show OK
freqtrade list-strategies --userdir user_data # all four should show OK
```

`joblib<1.6` is needed because joblib 1.6 removed a module Freqtrade's
hyperopt still imports.
`requirements.txt` pins Freqtrade and `joblib<1.6`. The joblib pin is needed
because joblib 1.6 removed a module Freqtrade's hyperopt still imports.

Create the private config (API secrets, gitignored). This never overwrites an existing file:

Expand Down Expand Up @@ -79,8 +103,9 @@ terminal, stop that one first (Ctrl+C) before using Start.
## 3. Data

Research picks pairs **point-in-time**, so it needs daily candles for every
Binance USDT pair, not just today's favourites. Do this once, then top up
occasionally:
Binance USDT pair, not just today's favourites. `python scripts/update_data.py`
runs all of the steps below (it's also the installer's **Update market data**
shortcut). Do this once, then top up monthly:

```bash
freqtrade list-pairs --userdir user_data --quote USDT --trading-mode spot --print-json > all_usdt.json
Expand Down Expand Up @@ -150,6 +175,36 @@ Then it must paper-trade for **4+ weeks** with results close to a replay of
the same weeks. If nothing passes, the answer is "don't go live yet" - not
"loosen the gates".

### Paper vs replay (the last gate)

Use the panel's **Paper trading → Paper vs replay** card, or run:

```bash
python scripts/reconcile.py <session-id>
```

It replays the exact period the session ran, with the same strategy, pairs and
starting balance. Then it matches the trades one by one (same pair, entries
within 1.5 candles). The session is "close to the replay" when all of these hold:

- it ran for at least 28 days
- at least 80% of trades happened in both
- realised return is within 2 percentage points of the replay
- fills cost at most 0.3% per side compared with the replay's candle prices

The result also splits the return gap into three parts:
- trades in both, which shows execution differences
- paper-only trades
- replay-only trades

It flags anything the replay can't reproduce:
- manual pauses, resumes and flattens
- drawdown halts
- strategy code or parameters that changed mid-session

The Research tab shows the newest result for each strategy.
See [docs/OPERATIONS.md](docs/OPERATIONS.md#paper-vs-replay).

Plain backtests still work as usual:

```bash
Expand Down Expand Up @@ -208,6 +263,7 @@ The panel has the same: **Performance tear sheet** on Paper trading, and **Open
```bash
python scripts/init_private_config.py
docker compose up -d --build # bot + panel (monitor mode) + watchdog
# or, without building: docker compose pull && docker compose up -d
```

The watchdog sends Telegram alerts when:
Expand All @@ -229,6 +285,7 @@ The tests cover:
- audit tamper detection
- a lookahead check: signals must not change when future candles are removed
- point-in-time universe selection
- paper-vs-replay trade matching, gap breakdown and checks
- the panel's dry-run guard and kill-switch sequence
- repo hygiene: no secrets, and no live config in git

Expand Down
Loading
Loading