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
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# The image only needs the Dockerfile; the repo is mounted at runtime.
*
!Dockerfile
38 changes: 38 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: CI

on:
push:
pull_request:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
cache-dependency-path: requirements-dev.txt

- name: Install
run: |
python -m pip install --upgrade pip
pip install "freqtrade==2026.8" "joblib<1.6" -r requirements-dev.txt

- name: Lint
run: ruff check .

- name: Throwaway private config
run: python scripts/init_private_config.py

- name: Strategies load
run: freqtrade list-strategies --userdir user_data -c user_data/config.json

- name: Tests
run: pytest
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,8 @@ user_data/plot/
*.docx
~$*.docx
all_usdt.json
reports/
user_data/audit/
user_data/risk_state.json
.pytest_cache/
.ruff_cache/
10 changes: 10 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# 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.
FROM freqtradeorg/freqtrade:2026.8

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

WORKDIR /ridgeline
ENTRYPOINT []
111 changes: 97 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@ A Freqtrade bot for Binance **spot**, with:
- three candidate strategies and an honest research pipeline to pick between them,
- a **paper-money control panel**: live paper trading with a fake wallet you
choose and can reset, plus fast historical replay,
- the official **FreqUI** dashboard for detailed live monitoring.
- the official **FreqUI** dashboard for detailed live monitoring,
- **operations tooling**:
- portfolio risk limits and a one-click kill switch
- a tamper-evident audit trail
- investor-style tear sheets
- Docker deployment with a watchdog and Telegram alerts
- tests and CI on every push

[![CI](https://github.com/devam2001/ridgeline/actions/workflows/ci.yml/badge.svg)](https://github.com/devam2001/ridgeline/actions/workflows/ci.yml)

Everything ships in **dry-run** mode. Nothing here touches real money; the
panel cannot start a live bot at all.
Expand All @@ -21,6 +29,11 @@ panel cannot start a live bot at all.
| `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 |
| `scripts/report.py`, `scripts/watchdog.py` | tear-sheet generator; health/risk watchdog with Telegram alerts |
| `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
Expand All @@ -36,18 +49,10 @@ freqtrade list-strategies --userdir user_data # all three should show OK
`joblib<1.6` is needed because joblib 1.6 removed a module Freqtrade's
hyperopt still imports.

If `user_data/config.private.json` doesn't exist, create it:

```json
{
"exchange": {"key": "", "secret": ""},
"api_server": {
"username": "freqtrader",
"password": "<long random string>",
"jwt_secret_key": "<64 random hex chars>",
"ws_token": "<random string>"
}
}
Create the private config (API secrets, gitignored). This never overwrites an existing file:

```bash
python scripts/init_private_config.py
```

## 2. Control panel (paper money + replay)
Expand Down Expand Up @@ -165,7 +170,85 @@ cooldown / StoplossGuard / MaxDrawdown protections.)
real fills and slippage match the paper results.
4. Turn on `telegram` in the config for entry/exit notifications.

## Risk controls (EmaRsiMomentum; the other strategies follow the same pattern)
## 6. Operations

### Risk limits and kill switch
Every strategy runs under portfolio-level limits, enforced in backtests too:
- **Max single position:** 35% of equity.
- **Max open risk:** 4.5% of equity, i.e. the total loss if every open stop hit at once.
- **Daily loss:** 3%. Hitting it pauses new entries until the next UTC day.
- **Drawdown halt:** 15%. Hitting it stops new entries until a human resumes.

The panel's **Risk controls** card shows live usage of each limit, with three controls:
- **Pause entries**
- **Resume**
- **Flatten all**: the kill switch. It market-closes everything and stops the bot.

### Audit trail
Every control action, risk event and watchdog alert is appended to a
hash-chained log in `user_data/audit/`. The panel shows whether the chain
still verifies.

### Reports
Investor-style tear sheets come as one self-contained HTML file. Use
Print → Save as PDF in your browser to get a PDF. Each covers:
- returns: monthly and year-to-date
- equity vs BTC, and drawdown
- Sharpe, Sortino and Calmar
- trade statistics and the full trade log

```bash
python scripts/report.py --db user_data/paper/<session>.sqlite --month 2026-10
python scripts/report.py --backtest user_data/replays/<job-id>
```

The panel has the same: **Performance tear sheet** on Paper trading, and **Open tear sheet** after a replay.

### Deployment and monitoring
```bash
python scripts/init_private_config.py
docker compose up -d --build # bot + panel (monitor mode) + watchdog
```

The watchdog sends Telegram alerts when:
- the bot is down or stalled
- the bot is paused or halted
- any risk limit is at 80% of its threshold or more

Setup, VPS hardening and the incident runbook are in
[docs/OPERATIONS.md](docs/OPERATIONS.md).

### Tests
```bash
pip install -r requirements-dev.txt
ruff check . && pytest
```

The tests cover:
- risk-limit boundaries
- audit tamper detection
- a lookahead check: signals must not change when future candles are removed
- point-in-time universe selection
- the panel's dry-run guard and kill-switch sequence
- repo hygiene: no secrets, and no live config in git

GitHub Actions runs them on every push.

## Managing other people's money

The code above is the operational plumbing a fund would expect. **It does not
make running other people's money legal.** In Australia, dealing in or
managing crypto-asset portfolios for others, copy trading, or pooling client
funds generally requires an Australian Financial Services Licence (AFSL).
Pooled funds are usually also a registered managed investment scheme (MIS),
with compliance, custody and audit obligations. A wholesale-only structure
reduces some obligations but doesn't remove the licence question. Other
countries have their own equivalents.

Talk to a financial-services lawyer before taking anyone else's capital.
This section is a pointer, not legal advice.

## Strategy risk settings (EmaRsiMomentum; the other strategies follow the same pattern)

- **1.5% risk per trade, ATR-adaptive** — the stoploss distance is no
longer a flat 5%; it's `min(8%, max(2%, ATR% × 2.5))`, computed per pair
Expand Down
50 changes: 35 additions & 15 deletions RESEARCH.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
**Setup**
- Long-only Binance spot, starting with $1,000, at most 3 trades open at once.
- 0.1% fee per trade, and protections switched on.
- **The portfolio risk limits are applied**, exactly as in paper and live
trading: 35% max position, 4.5% max open risk, 3% daily loss limit, 15%
drawdown halt. See `docs/OPERATIONS.md`.
- **In-sample (IS):** 2023-06-01 → 2025-06-01. Used for design and hyperopt.
- **Out-of-sample (OOS):** 2025-06-01 → 2026-10-10. Never used for tuning.
- **Pairs are chosen point-in-time.** Each window trades the top 25 USDT pairs
Expand All @@ -24,9 +27,24 @@

| Strategy | IS return | OOS return | OOS max DD | OOS profit factor | OOS trades | Walk-forward | Gates |
|---|---|---|---|---|---|---|---|
| **TrendRider4h** | +10.8% | **+19.3%** | **9.8%** | **1.47** | 99 | 71% | **8 / 9** |
| EmaRsiMomentum | +13.6% | +8.0% | 16.5% | 1.18 | 77 | 43% | 5 / 9 |
| TrendDipBuyer1h | −20.7% | −17.7% | 19.5% | 0.75 | 309 | 36% | 3 / 9 |
| **TrendRider4h** | +5.6% | **+17.3%** | **9.8%** | **1.43** | 99 | 64% | **8 / 9** |
| EmaRsiMomentum | +7.7% | +5.1% | 18.1% | 1.13 | 74 | 43% | 6 / 9 |
| TrendDipBuyer1h | −8.0% | −17.8% | 19.2% | 0.73 | 300 | 36% | 4 / 9 |

### What the risk limits changed

| | Without limits | With limits |
|---|---|---|
| TrendRider4h, OOS | +19.3% | **+17.3%** |
| TrendRider4h, IS | +10.8% | +5.6% |
| TrendRider4h, walk-forward | 71% | 64% |
| TrendDipBuyer1h, IS | −20.7% | **−8.0%** |

The limits **cost return on the good strategy**. The 35% position cap blocks
the large positions that risk-based sizing wants in low-volatility coins
(BTC, BNB, TRX). The limits **cut losses on the bad strategy**: the drawdown
halt more than halved TrendDipBuyer1h's in-sample loss. That trade-off is
the point of a risk layer.

**No strategy is eligible for real money yet.**

Expand All @@ -42,14 +60,15 @@ the next monthly re-run will settle it either way.
- Ranking by today's volume favours coins that pumped during the test.
- QNT (+133% in OOS) alone added +$210 from 5 trades.
- ZEC (+2,171%) pushed the equal-weight benchmark to +68%.
3. **Point-in-time top 25 (this run).** OOS +19.3%, IS +10.8%.
3. **Point-in-time top 25 (this run).** OOS +19.3%, IS +10.8% before the risk
limits; +17.3% and +5.6% with them.
- Pairs are chosen only from information available at the time.

The gap between the IS results is the most important finding:
- **+82.5% on the hand-picked pairs vs +10.8% point-in-time.** TrendRider4h
was hyperopted on the 15 hand-picked pairs, and a large part of that IS
result came from fitting those particular coins.
- **The OOS result barely moved (+21% → +19%).** It's the more trustworthy
- **+82.5% on the hand-picked pairs vs +10.8% point-in-time** (both without
risk limits). TrendRider4h was hyperopted on the 15 hand-picked pairs,
and a large part of that IS result came from fitting those particular coins.
- **The OOS result barely moved (+21% → +19% before risk limits).** It's the more trustworthy
number, but expect live results below even that.

## TrendRider4h in detail
Expand All @@ -61,24 +80,25 @@ The gap between the IS results is the most important finding:
|---|---|---|
| 2024-06 → 09 | −5.5% | −38% |
| 2024-12 → 2025-03 | +0.6% | −41% |
| 2025-09 → 12 | +0.7% | −43% |
| 2025-09 → 12 | −0.9% | −43% |
| 2025-12 → 2026-03 | 0.0%, no trades (BTC filter) | −31% |
| 2024-09 → 12 | +30.3% | +98% |
| 2024-09 → 12 | +30.7% | +98% |

In bull runs it catches only part of the move, as the last row shows.
- **Walk-forward:** profitable in 10 of 14 windows (71%), or 4 of 6 OOS-only.
Zero-trade windows don't count as wins. The worst window was −7.0%.
- **Walk-forward (with risk limits):** profitable in 9 of 14 windows (64%),
or 3 of 6 OOS-only. Zero-trade windows don't count as wins. The worst
window was −7.0%.
- **Robustness:**
- Still +17.3% OOS with fees raised by half.
- Still +15.4% OOS with fees raised by half.
- Freqtrade's lookahead check finds no bias.
- Indicators are stable whatever the warm-up length (worst deviation 0.2%).
- 4h vs 1h intra-candle detail gives near-identical results (+35.7% vs
+36.2%, measured on the earlier 26-pair set).
- **It trades rarely:** 5–7 trades a month. The stats are noisy at this sample size.

### The other two
- **EmaRsiMomentum** improves with point-in-time pairs (+8% OOS) but is
inconsistent: profitable in only 43% of windows, with a profit factor of 1.18.
- **EmaRsiMomentum** is positive OOS (+5.1%) but inconsistent: profitable
in only 43% of windows, with a profit factor of 1.13.
- **TrendDipBuyer1h** loses money in every configuration tested.

## Known limitations
Expand Down
61 changes: 61 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# 24/7 deployment: trading bot + monitor-mode panel + watchdog.
#
# python scripts/init_private_config.py # once: API secrets (gitignored)
# docker compose up -d --build
#
# Trades in whatever mode user_data/config.json says - dry_run: true (paper)
# unless you deliberately change it. Ports are published on 127.0.0.1 only;
# reach them from your laptop with an SSH tunnel (see docs/OPERATIONS.md).

x-ridgeline: &ridgeline
build: .
image: ridgeline:latest
restart: unless-stopped
volumes:
- .:/ridgeline
working_dir: /ridgeline
logging:
driver: json-file
options: { max-size: "10m", max-file: "5" }

services:
bot:
<<: *ridgeline
command:
- freqtrade
- trade
- --userdir=user_data
- --config=user_data/config.json
- --strategy=${RIDGELINE_STRATEGY:-TrendRider4h}
- --db-url=sqlite:////ridgeline/user_data/tradesv3.docker.sqlite
- --logfile=user_data/logs/bot.log
environment:
# listen inside the container network; publishing below stays on localhost
FREQTRADE__API_SERVER__LISTEN_IP_ADDRESS: "0.0.0.0"
ports:
- "127.0.0.1:8080:8080"
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/api/v1/ping', timeout=5)"]
interval: 60s
timeout: 10s
retries: 3
start_period: 180s

panel:
<<: *ridgeline
command: ["python", "-m", "uvicorn", "panel.app:app", "--host", "0.0.0.0", "--port", "8090"]
environment:
PANEL_MODE: monitor # Docker owns the bot process; kill switch still works
RIDGELINE_BOT_URL: http://bot:8080
ports:
- "127.0.0.1:8090:8090"
depends_on:
- bot

watchdog:
<<: *ridgeline
command: ["python", "scripts/watchdog.py"]
environment:
RIDGELINE_BOT_URL: http://bot:8080
depends_on:
- bot
Loading
Loading