← Back to Home
How to make Strategy Backtest Codes without Writing Code

How to make Strategy Backtest Codes without Writing Code

An automatic strategy builder separates trading rules from Python code. Instead of rewriting a strategy each time, the user changes a configuration file containing the symbol, indicator settings, entry rules, exit rules, risk limits, and initial capital.

The process has three parts:

  1. A JSON strategy file defines the rules.

  2. A backtest engine downloads historical data, calculates indicators, simulates trades, and saves results.

  3. A code generator embeds the chosen strategy into the engine and creates a standalone Python script.

I made a visual simple app for it that enables me to generate a library of strategy backtest codes quick and easy! You just make your strategy with the options in the interface and the code is generated automatically:

This design makes it easy to test multiple variations of the same idea. For example, a user can change RSI thresholds or moving-average periods without modifying the execution engine.

1. Define the strategy

Create a file called strategy.json.

{
  "name": "RSI Trend Strategy",
  "symbol": "SPY",
  "start": "2020-01-01",
  "end": "2024-12-31",
  "interval": "1d",
  "starting_cash": 100000,
  "commission": 0.001,
  "cash_percent": 95,
  "rsi_period": 14,
  "sma_period": 200,
  "buy_rsi_below": 30,
  "sell_rsi_above": 70,
  "stop_loss_pct": 5,
  "take_profit_pct": 12
}

This strategy buys SPY when RSI is below 30 and the closing price is above its 200-day moving average. It sells when RSI rises above 70, when the trade loses 5%, or when it gains 12%.

2. Create the backtest engine

Install the required packages:

pip install pandas yfinance matplotlib

Create backtest.py.

import json
from pathlib import Path

import matplotlib.pyplot as plt
import pandas as pd
import yfinance as yf


def run_backtest(config):
    output_dir = Path("backtest_output")
    output_dir.mkdir(exist_ok=True)

    data = yf.download(
        config["symbol"],
        start=config["start"],
        end=config["end"],
        interval=config["interval"],
        auto_adjust=False,
        progress=False,
    ).droplevel(1, 1)

    data = data[["Open", "High", "Low", "Close", "Volume"]].dropna()

    delta = data["Close"].diff()
    gain = delta.clip(lower=0).rolling(config["rsi_period"]).mean()
    loss = -delta.clip(upper=0).rolling(config["rsi_period"]).mean()

    rs = gain / loss.replace(0, float("nan"))
    data["RSI"] = 100 - (100 / (1 + rs))
    data["SMA"] = data["Close"].rolling(config["sma_period"]).mean()
    data = data.dropna()

    cash = float(config["starting_cash"])
    shares = 0
    entry_price = 0.0
    trades = []
    equity = []

    for date, row in data.iterrows():
        price = float(row["Close"])
        rsi = float(row["RSI"])
        sma = float(row["SMA"])

        if shares == 0:
            buy_signal = rsi < config["buy_rsi_below"] and price > sma

            if buy_signal:
                budget = cash * (float(config["cash_percent"]) / 100)
                shares = int(budget / (price * (1 + config["commission"])))

                if shares > 0:
                    cost = shares * price * (1 + config["commission"])
                    cash -= cost
                    entry_price = price

        else:
            stop_price = entry_price * (1 - config["stop_loss_pct"] / 100)
            target_price = entry_price * (1 + config["take_profit_pct"] / 100)

            sell_signal = (
                rsi > config["sell_rsi_above"]
                or price <= stop_price
                or price >= target_price
            )

            if sell_signal:
                proceeds = shares * price * (1 - config["commission"])
                pnl = proceeds - (shares * entry_price * (1 + config["commission"]))

                trades.append(
                    {
                        "entry_price": round(entry_price, 2),
                        "exit_price": round(price, 2),
                        "shares": shares,
                        "pnl": round(pnl, 2),
                        "return_pct": round(((price / entry_price) - 1) * 100, 2),
                        "exit_date": date.strftime("%Y-%m-%d"),
                    }
                )

                cash += proceeds
                shares = 0
                entry_price = 0.0

        portfolio_value = cash + (shares * price)

        equity.append(
            {
                "date": date.strftime("%Y-%m-%d"),
                "equity": round(portfolio_value, 2),
            }
        )

    if shares > 0:
        final_price = float(data["Close"].iloc[-1])
        proceeds = shares * final_price * (1 - config["commission"])
        pnl = proceeds - (shares * entry_price * (1 + config["commission"]))

        trades.append(
            {
                "entry_price": round(entry_price, 2),
                "exit_price": round(final_price, 2),
                "shares": shares,
                "pnl": round(pnl, 2),
                "return_pct": round(((final_price / entry_price) - 1) * 100, 2),
                "exit_date": data.index[-1].strftime("%Y-%m-%d"),
            }
        )

        cash += proceeds

    equity_df = pd.DataFrame(equity)
    trades_df = pd.DataFrame(trades)

    final_value = float(equity_df["equity"].iloc[-1])
    total_return = ((final_value / config["starting_cash"]) - 1) * 100

    equity_df["peak"] = equity_df["equity"].cummax()
    equity_df["drawdown_pct"] = (
        (equity_df["equity"] / equity_df["peak"] - 1) * 100
    )

    wins = int((trades_df["pnl"] > 0).sum()) if not trades_df.empty else 0
    total_trades = len(trades_df)
    win_rate = (wins / total_trades * 100) if total_trades else 0

    metrics = {
        "strategy": config["name"],
        "starting_cash": round(float(config["starting_cash"]), 2),
        "final_value": round(final_value, 2),
        "total_return_pct": round(total_return, 2),
        "max_drawdown_pct": round(float(equity_df["drawdown_pct"].min()), 2),
        "total_trades": total_trades,
        "win_rate_pct": round(win_rate, 2),
    }

    equity_df.to_csv(output_dir / "equity_curve.csv", index=False)
    trades_df.to_csv(output_dir / "trades.csv", index=False)

    with open(output_dir / "metrics.json", "w", encoding="utf-8") as file:
        json.dump(metrics, file, indent=2)

    plt.figure(figsize=(10, 4))
    plt.plot(pd.to_datetime(equity_df["date"]), equity_df["equity"])
    plt.title(config["name"])
    plt.ylabel("Portfolio Value")
    plt.grid(True, alpha=0.25)
    plt.tight_layout()
    plt.savefig(output_dir / "equity_curve.png", dpi=160)
    plt.close()

    return metrics


if __name__ == "__main__":
    CONFIG = json.loads(Path("strategy.json").read_text())
    results = run_backtest(CONFIG)
    print(json.dumps(results, indent=2))

Run the backtest:

python backtest.py

The script creates a backtest_output folder containing:

equity_curve.csv
equity_curve.png
metrics.json
trades.csv

3. Generate a standalone strategy script automatically

The next script reads strategy.json, inserts its contents into the backtest engine, and creates a new script called generated_strategy.py.

Create generator.py.

import json
from pathlib import Path

strategy = json.loads(Path("strategy.json").read_text())
engine_code = Path("backtest.py").read_text()

embedded_strategy = json.dumps(json.dumps(strategy))

generated_code = engine_code.replace(
    'CONFIG = json.loads(Path("strategy.json").read_text())',
    f"CONFIG = json.loads({embedded_strategy})",
)

Path("generated_strategy.py").write_text(generated_code, encoding="utf-8")

print("Created generated_strategy.py")

Run it with:

python generator.py

The generated file contains both the backtest logic and the selected strategy settings. It no longer needs strategy.json, which makes it useful for exporting, sharing, or versioning individual strategies.

Important improvements for real trading systems

This example is suitable for research and learning, but a production system should also include next-bar order execution, slippage, walk-forward testing, multiple assets, robust error handling, and protection against look-ahead bias.

A more advanced version can replace the fixed RSI and SMA rules with configurable condition groups. For example, users could define conditions such as RSI below 30, price above an EMA, MACD crossing above its signal line, or a trailing stop. The engine can then evaluate those conditions dynamically and the generator can export the exact strategy as Python code.

The key idea is simple: store strategy rules as data, let one engine execute them, and automatically export the final configuration into a reproducible script.