PrimoGreedy-Agent / backtest.py
CiscsoPonce's picture
Initial Deploy (Clean)
a2cbcac
from __future__ import annotations
import sys
from pathlib import Path
from src.backtesting import (
run_backtest,
PrimoAgentStrategy,
BuyAndHoldStrategy,
)
from src.backtesting.data import load_stock_data, load_all_data, list_available_stocks
from src.backtesting.plotting import plot_single_stock, plot_returns_bar_chart
from src.backtesting.reporting import generate_markdown_report
def _prompt(prompt: str) -> str:
try:
return input(prompt)
except EOFError:
return ""
def yes_no(question: str, default: bool = True) -> bool:
suffix = "[Y/n] " if default else "[y/N] "
while True:
ans = _prompt(f"{question} {suffix}").strip().lower()
if ans == "" and default is not None:
return default
if ans in {"y", "yes"}:
return True
if ans in {"n", "no"}:
return False
print("Please enter 'y' for yes or 'n' for no.")
def choose_mode() -> str:
print("\nSelect backtest mode:")
print(" 1) Single stock")
print(" 2) Multiple stocks")
print(" q) Quit")
while True:
choice = _prompt("> ").strip().lower()
if choice in {"1", "2", "q"}:
return choice
print("Invalid selection. Enter 1, 2 or q.")
def choose_symbol(available: list[str]) -> str | None:
print("\nAvailable symbols:")
for i, s in enumerate(available, 1):
print(f" {i:>2}) {s}")
print("Enter index or symbol (blank to cancel):")
while True:
val = _prompt("> ").strip()
if val == "":
return None
if val.isdigit():
idx = int(val)
if 1 <= idx <= len(available):
return available[idx - 1]
else:
up = val.upper()
if up in available:
return up
print("Invalid input. Try again.")
def choose_symbols_multi(available: list[str]) -> list[str] | None:
print("\nAvailable symbols:")
for i, s in enumerate(available, 1):
print(f" {i:>2}) {s}")
if yes_no("Run for ALL symbols in the list?", default=True):
return available
print("Enter comma-separated indices (e.g. 1,3,5), or blank to cancel:")
while True:
val = _prompt("> ").strip()
if val == "":
return None
try:
idxs = [int(x) for x in val.split(",") if x.strip()]
picked = []
for i in idxs:
if 1 <= i <= len(available):
picked.append(available[i - 1])
if picked:
# Ukloni duplikate uz očuvanje redoslijeda
seen = set()
uniq = []
for s in picked:
if s not in seen:
seen.add(s)
uniq.append(s)
return uniq
except ValueError:
pass
print("Invalid input. Try again.")
def pick_paths() -> tuple[Path, Path]:
# Determine data dir: prefer ./output/csv, fallback to ./data, then ./tests/data
preferred = Path("output/csv")
if preferred.exists():
data_dir = preferred
else:
alt1 = Path("data")
alt2 = Path("tests/data")
if alt1.exists():
print("'output/csv' not found. Using 'data/'.")
data_dir = alt1
elif alt2.exists():
print("'output/csv' and 'data/' not found. Using 'tests/data/'.")
data_dir = alt2
else:
print("No data directory found. Expected one of: output/csv, data, tests/data.")
raise SystemExit(1)
# Output directory (default: output/backtests)
default_output = Path("output/backtests")
use_default = yes_no(f"Use the default output directory '{default_output}'?", default=True)
if use_default:
output_dir = default_output
else:
# allow user to enter custom path
while True:
entered = _prompt("Enter output directory path: ").strip()
if entered:
output_dir = Path(entered)
break
print("Path cannot be empty.")
output_dir.mkdir(parents=True, exist_ok=True)
return data_dir, output_dir
def run_single_interactive(data_dir: Path, output_dir: Path) -> int:
available = list_available_stocks(str(data_dir))
if not available:
print(f"No CSV files available in '{data_dir}'.")
return 1
symbol = choose_symbol(available)
if not symbol:
print("Cancelled.")
return 0
printlog = yes_no("Enable detailed strategy logs?", default=False)
ohlc_data, signals_df = load_stock_data(symbol, str(data_dir))
if ohlc_data is None or signals_df is None:
return 1
print(f"\nPRIMOAGENT SINGLE STOCK BACKTEST - {symbol}")
print("=" * 60)
primo_results, primo_cerebro = run_backtest(
ohlc_data, PrimoAgentStrategy, "PrimoAgent", signals_df=signals_df, printlog=printlog
)
buyhold_results, buyhold_cerebro = run_backtest(
ohlc_data, BuyAndHoldStrategy, "Buy & Hold"
)
print("\nPerformance comparison")
print("-" * 65)
metrics = [
"Cumulative Return [%]",
"Annual Volatility [%]",
"Max Drawdown [%]",
"Sharpe Ratio",
"Total Trades",
]
print(f"{'Metric':<22} {'PrimoAgent':>12} {'Buy & Hold':>12} {'Difference':>12}")
for m in metrics:
pv, bv = primo_results[m], buyhold_results[m]
diff = pv - bv
if "[%]" in m or "Ratio" in m:
print(f"{m:<22} {pv:>12.2f} {bv:>12.2f} {diff:>+12.2f}")
else:
print(f"{m:<22} {pv:>12.0f} {bv:>12.0f} {diff:>+12.0f}")
rel = primo_results["Cumulative Return [%]"] - buyhold_results["Cumulative Return [%]"]
if rel > 0:
print(f"\nPrimoAgent OUTPERFORMED Buy & Hold by {rel:+.2f}%!")
else:
print(f"\nPrimoAgent underperformed Buy & Hold by {abs(rel):.2f}%")
chart_path = plot_single_stock(symbol, primo_cerebro, buyhold_cerebro, str(output_dir))
print(f"Chart saved: {chart_path}")
return 0
def run_multi_interactive(data_dir: Path, output_dir: Path) -> int:
available = list_available_stocks(str(data_dir))
if not available:
print(f"No CSV files available in '{data_dir}'.")
return 1
symbols = choose_symbols_multi(available)
if not symbols:
print("Cancelled.")
return 0
printlog = yes_no("Enable detailed strategy logs?", default=False)
print("\nPRIMOAGENT MULTI-STOCK BACKTEST")
print("=" * 50)
print(f"Selected: {', '.join(symbols)}")
all_results = {}
for symbol in symbols:
print(f"\n{'=' * 60}\nProcessing {symbol}\n{'=' * 60}")
ohlc_data, signals_df = load_stock_data(symbol, str(data_dir))
if ohlc_data is None or signals_df is None:
print(f"Skipping {symbol} (no data)")
continue
try:
primo_results, primo_cerebro = run_backtest(
ohlc_data, PrimoAgentStrategy, f"{symbol} PrimoAgent", signals_df=signals_df, printlog=printlog
)
buyhold_results, buyhold_cerebro = run_backtest(
ohlc_data, BuyAndHoldStrategy, f"{symbol} Buy & Hold"
)
all_results[symbol] = {"primo": primo_results, "buyhold": buyhold_results}
# individual chart
_ = plot_single_stock(symbol, primo_cerebro, buyhold_cerebro, str(output_dir), f"backtest_results_{symbol}.png")
# quick comparison
primo_return = primo_results["Cumulative Return [%]"]
buyhold_return = buyhold_results["Cumulative Return [%]"]
rel = primo_return - buyhold_return
if rel > 0:
print(f"{symbol}: PrimoAgent +{rel:.2f}% ( {primo_return:.2f}% vs {buyhold_return:.2f}% )")
else:
print(f"{symbol}: PrimoAgent -{abs(rel):.2f}% ( {primo_return:.2f}% vs {buyhold_return:.2f}% )")
except Exception as e:
print(f"Error for {symbol}: {e}")
if not all_results:
print("No successful backtests.")
return 1
# aggregate chart and report
bar_chart_path = output_dir / "returns_comparison.png"
plot_returns_bar_chart(all_results, bar_chart_path)
print(f"Returns comparison chart saved: {bar_chart_path}")
report_path = output_dir / "backtest_analysis_report.md"
generate_markdown_report(all_results, report_path)
print(f"Report saved: {report_path}")
total = len(all_results)
wins = sum(1 for r in all_results.values() if r["primo"]["Cumulative Return [%]"] > r["buyhold"]["Cumulative Return [%]"])
avg_primo = sum(r["primo"]["Cumulative Return [%]"] for r in all_results.values()) / total
avg_bh = sum(r["buyhold"]["Cumulative Return [%]"] for r in all_results.values()) / total
print("\nCOMPLETE")
print("=" * 50)
print(f"Stocks: {total}")
print(f"PrimoAgent wins: {wins}/{total} ({wins/total*100:.1f}%)")
print(f"Avg PrimoAgent return: {avg_primo:.2f}% | Buy & Hold: {avg_bh:.2f}%")
print(f"Relative: {avg_primo - avg_bh:+.2f}%")
print(f"Outputs: {output_dir.resolve()}")
return 0
def main() -> int:
print("PrimoAgent Backtest (interactive mode)")
data_dir, output_dir = pick_paths()
mode = choose_mode()
if mode == "q":
print("Goodbye.")
return 0
if mode == "1":
return run_single_interactive(data_dir, output_dir)
if mode == "2":
return run_multi_interactive(data_dir, output_dir)
return 0
if __name__ == "__main__":
raise SystemExit(main())