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:
A JSON strategy file defines the rules.
A backtest engine downloads historical data, calculates indicators, simulates trades, and saves results.
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.
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%.
Install the required packages:
pip install pandas yfinance matplotlibCreate 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.pyThe script creates a backtest_output folder
containing:
equity_curve.csv
equity_curve.png
metrics.json
trades.csv
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.pyThe 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.
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.