Add private Freqtrade scaffold for partner backtesting.

Ship SampleStrategy and Integral workflow scripts without proprietary V15 logic, Pine, or run results.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Artemii Peretiachenko 2026-08-02 23:55:44 +02:00
commit 99de26e7f0
13 changed files with 831 additions and 0 deletions

41
.gitignore vendored Normal file
View file

@ -0,0 +1,41 @@
# Freqtrade runtime / data
user_data/data/
user_data/logs/
user_data/plot/
user_data/backtest_results/
user_data/hyperopt_results/
user_data/hyperopts/
user_data/strategies/*.json
user_data/notebooks/
user_data/*.sqlite
user_data/*.sqlite-journal
user_data/tradesv3.dryrun.sqlite*
user_data/freqtradeservice.json
user_data/hyperopt.lock
# Proprietary strategy + Pine (local only — do not push)
user_data/strategies/V15_5_LTF*
v15_5_LTF.txt
*.bak
*.bak_*
# Local backtest / hyperopt run dumps
results/
# Secrets / local overrides
user_data/config_private.json
.env
*.pem
# Python
__pycache__/
*.py[cod]
*.egg-info/
.venv/
venv/
# OS / IDE
.DS_Store
.idea/
.vscode/
*.swp

106
README.md Normal file
View file

@ -0,0 +1,106 @@
# 428 Backtester — Integral (Freqtrade)
Local Freqtrade scaffold for backtesting on **Binance Futures BTC/USDT:USDT**, **15m** (+ **6h** informative data), built around the Integral workflow.
No Jupyter. Reports come from Freqtrade CLI + HTML plots (`plot-profit`, trade chart).
Ship a **SampleStrategy** by default. Drop your own strategy under `user_data/strategies/` and point scripts at it with `STRATEGY=YourClassName`.
## Requirements
- **Preferred:** Docker + Docker Compose (`freqtradeorg/freqtrade:stable_plot`)
- **Fallback:** Python 3.12 venv with `freqtrade` + `plotly` (scripts use this automatically if Docker is missing)
- ~2+ GB disk for OHLCV history
### Local venv setup (no Docker)
```bash
/opt/homebrew/opt/python@3.12/bin/python3.12 -m venv .venv
source .venv/bin/activate
pip install -U pip 'freqtrade[hyperopt]' plotly
```
## Quick start
```bash
# 1) Download futures candles (15m + 6h). Default timerange from 2024-07-01.
./scripts/download_data.sh
# 2) Run baseline backtest (SampleStrategy; full history from 2024-07-01)
./scripts/backtest.sh
# 3) Hyperopt buy/sell params on in-sample range (default 20240701-20260101)
./scripts/hyperopt.sh
# EPOCHS=200 LOSS=SharpeHyperOptLossDaily ./scripts/hyperopt.sh
# 4) Apply best epoch params, then OOS backtest (default 20260101-)
./scripts/apply_hyperopt_params.sh
TIMERANGE=20260101- ./scripts/backtest.sh
# 5) Equity + trade charts (pick a shorter range for readable plots)
TIMERANGE=20250101-20250201 ./scripts/plot.sh
```
Scripts auto-detect Docker; if absent they use `.venv/bin/freqtrade`.
### Your strategy
```bash
# Place YourStrategy.py in user_data/strategies/
STRATEGY=YourStrategy ./scripts/backtest.sh
STRATEGY=YourStrategy ./scripts/hyperopt.sh
```
Or set `"strategy": "YourStrategy"` in `user_data/config.json` / `docker-compose.yml`.
### Custom timerange
```bash
TIMERANGE=20240101-20250601 ./scripts/download_data.sh
TIMERANGE=20240101-20250601 ./scripts/backtest.sh
```
### Direct docker compose
```bash
docker compose run --rm freqtrade download-data \
--config /freqtrade/user_data/config.json \
--trading-mode futures -t 15m 6h -p BTC/USDT:USDT --timerange 20240701-
docker compose run --rm freqtrade backtesting \
--config /freqtrade/user_data/config.json \
--strategy SampleStrategy --timeframe 15m --timerange 20240701-
```
## Project layout
| Path | Role |
|------|------|
| [`user_data/strategies/SampleStrategy.py`](user_data/strategies/SampleStrategy.py) | Placeholder strategy (replace with yours) |
| [`user_data/config.json`](user_data/config.json) | Binance futures dry-run / backtest config |
| [`scripts/`](scripts/) | download / backtest / hyperopt / plot helpers |
| [`docker-compose.yml`](docker-compose.yml) | `freqtradeorg/freqtrade:stable_plot` |
## Optimization
Primary tool: **Freqtrade Hyperopt** (Optuna TPE) over strategy `IntParameter` / `DecimalParameter` spaces.
Default split used by scripts:
- **IS / hyperopt:** `TIMERANGE=20240701-20260101`
- **OOS backtest:** `TIMERANGE=20260101-`
```bash
./scripts/hyperopt.sh # IS search
./scripts/apply_hyperopt_params.sh # write user_data/strategies/<Strategy>.json
TIMERANGE=20260101- ./scripts/backtest.sh # OOS with best params
# Defaults again: remove the JSON override
rm -f user_data/strategies/SampleStrategy.json
```
If optimizable params change `populate_indicators` (not only entry/exit columns), keep `--analyze-per-epoch` (default in `hyperopt.sh`). Without it every epoch can repeat the baseline result.
## Notes
- **Fees / funding:** `config.json` sets `fee: 0.0005` (5 bps). Funding rates download with futures data when available; treat equity as approximate.
- **Private logic:** proprietary strategies and run artifacts stay local (see `.gitignore`). Do not commit them to this repo.

18
docker-compose.yml Normal file
View file

@ -0,0 +1,18 @@
---
services:
freqtrade:
# _plot image includes plotly for plot-profit / plot-dataframe
image: freqtradeorg/freqtrade:stable_plot
restart: "no"
container_name: freqtrade-backtester
volumes:
- "./user_data:/freqtrade/user_data"
ports:
- "127.0.0.1:8080:8080"
# Default: no long-running trade process; use scripts/*.sh via docker compose run
command: >
trade
--logfile /freqtrade/user_data/logs/freqtrade.log
--db-url sqlite:////freqtrade/user_data/tradesv3.sqlite
--config /freqtrade/user_data/config.json
--strategy SampleStrategy

3
requirements.txt Normal file
View file

@ -0,0 +1,3 @@
freqtrade[hyperopt]>=2025.1
plotly>=5.0
scipy>=1.11

21
scripts/_env.sh Executable file
View file

@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Resolve freqtrade binary: prefer docker compose, else local .venv
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
freqtrade() {
docker compose run --rm freqtrade "$@"
}
elif [[ -x "$ROOT/.venv/bin/freqtrade" ]]; then
# shellcheck disable=SC1091
source "$ROOT/.venv/bin/activate"
freqtrade() {
"$ROOT/.venv/bin/freqtrade" "$@"
}
else
echo "Neither Docker nor .venv/bin/freqtrade found." >&2
echo "Install Docker Desktop, or: python3.12 -m venv .venv && .venv/bin/pip install freqtrade plotly" >&2
exit 1
fi

View file

@ -0,0 +1,83 @@
#!/usr/bin/env bash
set -euo pipefail
# shellcheck disable=SC1091
source "$(dirname "$0")/_env.sh"
# Export best (or N-th) hyperopt epoch into strategy params JSON so backtesting
# picks them up: user_data/strategies/<Strategy>.json
STRATEGY="${STRATEGY:-SampleStrategy}"
EPOCH="${EPOCH:--1}"
OUT="${OUT:-user_data/strategies/${STRATEGY}.json}"
TMP="$(mktemp)"
trap 'rm -f "$TMP"' EXIT
freqtrade hyperopt-show \
--config user_data/config.json \
-n "${EPOCH}" \
--print-json \
--no-header >"$TMP"
PYTHON_BIN="${ROOT}/.venv/bin/python"
if [[ ! -x "${PYTHON_BIN}" ]]; then
PYTHON_BIN="$(command -v python3)"
fi
"${PYTHON_BIN}" - "$TMP" "$STRATEGY" "$OUT" <<'PY'
import json, sys
from pathlib import Path
src, strategy, out = sys.argv[1], sys.argv[2], sys.argv[3]
raw = Path(src).read_text().strip()
# hyperopt-show may print log noise; keep the last JSON object
start = raw.rfind("{")
if start < 0:
raise SystemExit(f"No JSON found in hyperopt-show output:\n{raw[:500]}")
payload = json.loads(raw[start:])
KNOWN = ("buy", "sell", "roi", "stoploss", "trailing", "protection")
def expand_tp_combo(sell: dict) -> dict:
"""If sell has tp_rr_combo 'a,b,c', mirror into tp1_rr/tp2_rr/tp3_rr."""
combo = sell.get("tp_rr_combo")
if combo is None:
return sell
if isinstance(combo, (list, tuple)) and len(combo) == 3:
a, b, c = (float(x) for x in combo)
sell["tp_rr_combo"] = f"{a},{b},{c}"
elif isinstance(combo, str) and "," in combo:
a, b, c = (float(x) for x in combo.split(","))
else:
return sell
sell["tp1_rr"] = a
sell["tp2_rr"] = b
sell["tp3_rr"] = c
return sell
def as_params(obj: dict) -> dict:
if "params" in obj and isinstance(obj["params"], dict):
raw_params = obj["params"]
if any(k in raw_params for k in KNOWN):
params = {k: v for k, v in raw_params.items() if k in KNOWN}
else:
params = {"buy": raw_params} if raw_params else {}
elif any(k in obj for k in KNOWN):
params = {k: v for k, v in obj.items() if k in KNOWN}
else:
params = {"buy": obj} if obj else {}
if "sell" in params and isinstance(params["sell"], dict):
params["sell"] = expand_tp_combo(dict(params["sell"]))
return params
params = as_params(payload)
doc = {
"strategy_name": strategy,
"params": params,
}
Path(out).write_text(json.dumps(doc, indent=2) + "\n")
print(f"Wrote {out}")
print(json.dumps(params, indent=2))
PY

16
scripts/backtest.sh Executable file
View file

@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
# shellcheck disable=SC1091
source "$(dirname "$0")/_env.sh"
TIMERANGE="${TIMERANGE:-20240701-}"
STRATEGY="${STRATEGY:-SampleStrategy}"
freqtrade backtesting \
--config user_data/config.json \
--strategy "${STRATEGY}" \
--timeframe 15m \
--timerange "${TIMERANGE}" \
--breakdown day \
--cache none \
"$@"

15
scripts/download_data.sh Executable file
View file

@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
# shellcheck disable=SC1091
source "$(dirname "$0")/_env.sh"
TIMERANGE="${TIMERANGE:-20240701-}"
PAIRS="${PAIRS:-BTC/USDT:USDT}"
freqtrade download-data \
--config user_data/config.json \
--exchange binance \
--trading-mode futures \
--pairs ${PAIRS} \
--timeframes 15m 6h \
--timerange "${TIMERANGE}"

38
scripts/hyperopt.sh Executable file
View file

@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
# shellcheck disable=SC1091
source "$(dirname "$0")/_env.sh"
# In-sample range for hyperopt; use a later TIMERANGE for OOS backtest.
# --analyze-per-epoch is required when buy/sell params change populate_indicators
# (not only entry/exit columns). Set ANALYZE_PER_EPOCH=0 to disable.
TIMERANGE="${TIMERANGE:-20240701-20260101}"
STRATEGY="${STRATEGY:-SampleStrategy}"
EPOCHS="${EPOCHS:-100}"
SPACES="${SPACES:-buy sell}"
LOSS="${LOSS:-SharpeHyperOptLossDaily}"
JOBS="${JOBS:-}"
ANALYZE_PER_EPOCH="${ANALYZE_PER_EPOCH:-1}"
# shellcheck disable=SC2206
SPACES_ARR=(${SPACES})
ARGS=(
--config user_data/config.json
--strategy "${STRATEGY}"
--timeframe 15m
--timerange "${TIMERANGE}"
--spaces "${SPACES_ARR[@]}"
--hyperopt-loss "${LOSS}"
-e "${EPOCHS}"
)
if [[ "${ANALYZE_PER_EPOCH}" != "0" ]]; then
ARGS+=(--analyze-per-epoch)
fi
if [[ -n "${JOBS}" ]]; then
ARGS+=(-j "${JOBS}")
fi
freqtrade hyperopt "${ARGS[@]}" "$@"

32
scripts/plot.sh Executable file
View file

@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
# shellcheck disable=SC1091
source "$(dirname "$0")/_env.sh"
TIMERANGE="${TIMERANGE:-20250101-20250201}"
PAIR="${PAIR:-BTC/USDT:USDT}"
STRATEGY="${STRATEGY:-SampleStrategy}"
mkdir -p user_data/plot
echo "==> plot-profit (equity curve)"
freqtrade plot-profit \
--config user_data/config.json \
--strategy "${STRATEGY}" \
--timerange "${TIMERANGE}" \
--timeframe 15m \
"$@"
echo "==> trade chart (price / signals / SL·TP / fills / volume)"
PYTHON_BIN="${ROOT}/.venv/bin/python"
if [[ ! -x "${PYTHON_BIN}" ]]; then
PYTHON_BIN="python3"
fi
"${PYTHON_BIN}" "$(dirname "$0")/plot_trades_chart.py" \
--config user_data/config.json \
--strategy "${STRATEGY}" \
--pair "${PAIR}" \
--timerange "${TIMERANGE}" \
--timeframe 15m
echo "HTML plots are under user_data/plot/"

View file

@ -0,0 +1,335 @@
#!/usr/bin/env python3
"""
Clean trade chart: price, signals, SL/TP levels, filled trades, volume.
Price / signals / SL·TP / fills no indicator clutter.
"""
from __future__ import annotations
import argparse
import logging
import sys
from datetime import UTC, datetime
from pathlib import Path
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from freqtrade.configuration import Configuration
from freqtrade.data.btanalysis import extract_trades_of_period
from freqtrade.data.converter import trim_dataframe
from freqtrade.data.dataprovider import DataProvider
from freqtrade.misc import pair_to_filename
from freqtrade.plot.plotting import init_plotscript, store_plot_file
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.strategy import IStrategy
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger("plot_trades_chart")
SIGNAL_SIZE = 16
TRADE_SIZE = 14
LEVEL_STYLES = {
"sl": ("SL", "#e74c3c", "solid", 2),
"tp1": ("TP1", "#27ae60", "solid", 1.5),
"tp2": ("TP2", "#2ecc71", "dash", 1.2),
"tp3": ("TP3", "#1abc9c", "dot", 1.2),
}
def _signal_scatter(
data: pd.DataFrame, column: str, color: str, direction: str, size: int
) -> go.Scatter | None:
if column not in data.columns:
return None
df = data[data[column] == 1]
if df.empty:
return None
return go.Scatter(
x=df["date"],
y=df["close"],
mode="markers",
name=column,
marker=dict(
symbol=f"triangle-{direction}-dot",
size=size,
line=dict(width=1.5, color=color),
color=color,
),
)
def _levels_for_trade(row: pd.Series, candles: pd.DataFrame) -> dict[str, float] | None:
"""Read frozen SL/TP from the entry candle (strategy columns)."""
open_ts = pd.Timestamp(row["open_date"])
if open_ts.tzinfo is None:
open_ts = open_ts.tz_localize("UTC")
dates = pd.to_datetime(candles["date"], utc=True)
matched = candles.loc[dates <= open_ts]
if matched.empty:
return None
candle = matched.iloc[-1]
prefix = "short" if bool(row.get("is_short", False)) else "long"
out: dict[str, float] = {}
for key in ("sl", "tp1", "tp2", "tp3"):
col = f"{prefix}_{key}"
if col not in candle.index or pd.isna(candle[col]):
return None
out[key] = float(candle[col])
return out
def _tp1_fill_time(row: pd.Series) -> pd.Timestamp | None:
orders = row.get("orders")
if not isinstance(orders, list):
return None
for order in orders:
tag = str(order.get("ft_order_tag") or "")
if tag == "tp1" or tag.startswith("tp1"):
ts = order.get("order_filled_timestamp") or order.get("order_filled_date")
if ts is None:
return None
if isinstance(ts, (int, float)):
return pd.to_datetime(ts, unit="ms", utc=True)
return pd.to_datetime(ts, utc=True)
return None
def _add_trade_levels(fig: go.Figure, trades: pd.DataFrame, candles: pd.DataFrame) -> None:
seen: set[str] = set()
for _, row in trades.iterrows():
levels = _levels_for_trade(row, candles)
if not levels:
continue
x0, x1 = row["open_date"], row["close_date"]
if pd.isna(x1):
continue
be_from = _tp1_fill_time(row)
for key, price in levels.items():
name, color, dash, width = LEVEL_STYLES[key]
show = name not in seen
if show:
seen.add(name)
if key == "sl" and be_from is not None and be_from > x0:
fig.add_trace(
go.Scatter(
x=[x0, be_from],
y=[price, price],
mode="lines",
name=name,
showlegend=show,
line=dict(color=color, width=width, dash=dash),
hovertemplate=f"{name}: %{{y:.1f}}<extra></extra>",
),
row=1,
col=1,
)
be_name = "SL (BE)"
be_show = be_name not in seen
if be_show:
seen.add(be_name)
entry = float(row["open_rate"])
fig.add_trace(
go.Scatter(
x=[be_from, x1],
y=[entry, entry],
mode="lines",
name=be_name,
showlegend=be_show,
line=dict(color="#f39c12", width=2, dash="dash"),
hovertemplate=f"{be_name}: %{{y:.1f}}<extra></extra>",
),
row=1,
col=1,
)
else:
fig.add_trace(
go.Scatter(
x=[x0, x1],
y=[price, price],
mode="lines",
name=name,
showlegend=show,
line=dict(color=color, width=width, dash=dash),
hovertemplate=f"{name}: %{{y:.1f}}<extra></extra>",
),
row=1,
col=1,
)
def _add_filled_trades(fig: go.Figure, trades: pd.DataFrame) -> None:
if trades is None or trades.empty:
return
desc = trades.apply(
lambda r: (
f"{r['profit_ratio']:.2%}, "
+ (f"{r['enter_tag']}, " if pd.notna(r.get('enter_tag')) else "")
+ f"{r['exit_reason']}, "
+ f"{r['trade_duration']} min"
),
axis=1,
)
fig.add_trace(
go.Scatter(
x=trades["open_date"],
y=trades["open_rate"],
mode="markers",
name="Trade entry",
text=desc,
marker=dict(symbol="circle-open", size=TRADE_SIZE, line=dict(width=2.5), color="cyan"),
),
row=1,
col=1,
)
wins = trades["profit_ratio"] > 0
losses = ~wins
if wins.any():
fig.add_trace(
go.Scatter(
x=trades.loc[wins, "close_date"],
y=trades.loc[wins, "close_rate"],
mode="markers",
name="Exit - Profit",
text=desc[wins],
marker=dict(
symbol="square-open", size=TRADE_SIZE, line=dict(width=2.5), color="green"
),
),
row=1,
col=1,
)
if losses.any():
fig.add_trace(
go.Scatter(
x=trades.loc[losses, "close_date"],
y=trades.loc[losses, "close_rate"],
mode="markers",
name="Exit - Loss",
text=desc[losses],
marker=dict(
symbol="square-open", size=TRADE_SIZE, line=dict(width=2.5), color="red"
),
),
row=1,
col=1,
)
def build_figure(pair: str, data: pd.DataFrame, trades: pd.DataFrame) -> go.Figure:
fig = make_subplots(
rows=2,
cols=1,
shared_xaxes=True,
row_width=[1, 4],
vertical_spacing=0.02,
)
fig.update_layout(
title=f"{pair} — trades",
xaxis_rangeslider_visible=False,
legend=dict(orientation="h", yanchor="bottom", y=1.02, x=0),
margin=dict(t=80, b=40),
modebar_add=["v1hovermode", "toggleSpikeLines"],
)
fig.update_yaxes(title_text="Price", row=1, col=1)
fig.update_yaxes(title_text="Volume", row=2, col=1)
fig.add_trace(
go.Candlestick(
x=data["date"],
open=data["open"],
high=data["high"],
low=data["low"],
close=data["close"],
name="Price",
increasing_line_color="#26a69a",
decreasing_line_color="#ef5350",
),
row=1,
col=1,
)
for scatter in (
_signal_scatter(data, "enter_long", "#2ecc71", "up", SIGNAL_SIZE),
_signal_scatter(data, "exit_long", "#e74c3c", "down", SIGNAL_SIZE),
_signal_scatter(data, "enter_short", "#3498db", "down", SIGNAL_SIZE),
_signal_scatter(data, "exit_short", "#9b59b6", "up", SIGNAL_SIZE),
):
if scatter is not None:
fig.add_trace(scatter, row=1, col=1)
if trades is not None and not trades.empty:
_add_trade_levels(fig, trades, data)
_add_filled_trades(fig, trades)
fig.add_trace(
go.Bar(
x=data["date"],
y=data["volume"],
name="Volume",
marker_color="DarkSlateGrey",
marker_line_color="DarkSlateGrey",
),
row=2,
col=1,
)
return fig
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--config", default="user_data/config.json")
parser.add_argument("--strategy", default="SampleStrategy")
parser.add_argument("--timerange", default=None)
parser.add_argument("--pair", default="BTC/USDT:USDT")
parser.add_argument("--timeframe", default="15m")
parser.add_argument("--outfile", default=None)
args = parser.parse_args()
cfg = Configuration.from_files([args.config])
cfg["strategy"] = args.strategy
cfg["timeframe"] = args.timeframe
cfg["pairs"] = [args.pair]
if args.timerange:
cfg["timerange"] = args.timerange
cfg.setdefault("trade_source", "file")
strategy = StrategyResolver.load_strategy(cfg)
exchange = ExchangeResolver.load_exchange(cfg)
IStrategy.dp = DataProvider(cfg, exchange)
strategy.ft_bot_start()
strategy_safe_wrapper(strategy.bot_loop_start)(current_time=datetime.now(UTC))
plot_elements = init_plotscript(cfg, list(exchange.markets), strategy.startup_candle_count)
timerange = plot_elements["timerange"]
trades = plot_elements["trades"]
pair = args.pair
if pair not in plot_elements["ohlcv"]:
raise SystemExit(f"No OHLCV for {pair}")
data = strategy.analyze_ticker(plot_elements["ohlcv"][pair], {"pair": pair})
data = trim_dataframe(data, timerange)
if not trades.empty:
trades_pair = trades.loc[trades["pair"] == pair]
trades_pair = extract_trades_of_period(data, trades_pair)
else:
trades_pair = trades
fig = build_figure(pair, data, trades_pair)
out_name = args.outfile or f"freqtrade-plot-{pair_to_filename(pair)}-{args.timeframe}.html"
store_plot_file(fig, filename=out_name, directory=Path(cfg["user_data_dir"]) / "plot")
logger.info("Open: user_data/plot/%s", out_name)
return 0
if __name__ == "__main__":
raise SystemExit(main())

60
user_data/config.json Normal file
View file

@ -0,0 +1,60 @@
{
"max_open_trades": 1,
"stake_currency": "USDT",
"stake_amount": "unlimited",
"tradable_balance_ratio": 0.99,
"fiat_display_currency": "USD",
"dry_run": true,
"dry_run_wallet": 10000,
"cancel_open_orders_on_exit": false,
"trading_mode": "futures",
"margin_mode": "isolated",
"unfilledtimeout": {
"entry": 10,
"exit": 10,
"exit_timeout_count": 0,
"unit": "minutes"
},
"entry_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"exit_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1
},
"exchange": {
"name": "binance",
"key": "",
"secret": "",
"ccxt_config": {},
"ccxt_async_config": {},
"pair_whitelist": [
"BTC/USDT:USDT"
],
"pair_blacklist": []
},
"pairlists": [
{
"method": "StaticPairList"
}
],
"timeframe": "15m",
"dataformat_ohlcv": "feather",
"dataformat_trades": "feather",
"fee": 0.0005,
"strategy": "SampleStrategy",
"bot_name": "428-backtester",
"initial_state": "running",
"force_entry_enable": false,
"internals": {
"process_throttle_secs": 5
}
}

View file

@ -0,0 +1,63 @@
"""
Minimal sample strategy for the 428 / Integral backtester scaffold.
Replace this file (or add your own under user_data/strategies/) and set
STRATEGY=<ClassName> when running scripts. Defaults in config / compose
point here so a fresh clone backtests without proprietary logic.
"""
from __future__ import annotations
from pandas import DataFrame
from freqtrade.strategy import IStrategy, IntParameter
import talib.abstract as ta
class SampleStrategy(IStrategy):
"""EMA crossover + RSI filter — placeholder only, not a production system."""
INTERFACE_VERSION = 3
timeframe = "15m"
can_short = True
minimal_roi = {"0": 0.04, "60": 0.02, "180": 0.01, "360": 0}
stoploss = -0.03
trailing_stop = False
process_only_new_candles = True
startup_candle_count = 50
buy_rsi = IntParameter(20, 40, default=30, space="buy", optimize=True)
sell_rsi = IntParameter(60, 80, default=70, space="sell", optimize=True)
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["ema_fast"] = ta.EMA(dataframe, timeperiod=12)
dataframe["ema_slow"] = ta.EMA(dataframe, timeperiod=26)
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(dataframe["ema_fast"] > dataframe["ema_slow"])
& (dataframe["rsi"] < self.buy_rsi.value)
& (dataframe["volume"] > 0),
"enter_long",
] = 1
dataframe.loc[
(dataframe["ema_fast"] < dataframe["ema_slow"])
& (dataframe["rsi"] > self.sell_rsi.value)
& (dataframe["volume"] > 0),
"enter_short",
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(dataframe["ema_fast"] < dataframe["ema_slow"]) & (dataframe["volume"] > 0),
"exit_long",
] = 1
dataframe.loc[
(dataframe["ema_fast"] > dataframe["ema_slow"]) & (dataframe["volume"] > 0),
"exit_short",
] = 1
return dataframe