From fc54074a59690f9740502ac5b511faefeec8c7c0 Mon Sep 17 00:00:00 2001 From: henry Date: Fri, 23 Jan 2026 18:22:56 +0100 Subject: [PATCH] ADD: added optimizer workflow --- .gitignore | 3 + Backtest_Report_AAPL.html | 61 ----------------- Backtest_Report_TSLA.html | 61 ----------------- Backtest_Trades_AAPL.xlsx | Bin 5361 -> 0 bytes Backtest_Trades_TSLA.xlsx | Bin 5333 -> 0 bytes backtester.py | 138 +++++++++++++++++++++++++++++++++----- batch_optimizer.py | 115 +++++++++++++++++++++++++++++++ checkStocks.py | 2 +- markt_scan_20260121.xlsx | Bin 5219 -> 0 bytes optimizer.py | 115 ++++++++++++++++++++----------- optimizers.py | 69 +++++++++++++++++++ strategies.py | 92 +++++++++++++++++++++++++ tickers.json | 6 ++ tickers1.json | 19 ++++++ 14 files changed, 503 insertions(+), 178 deletions(-) delete mode 100644 Backtest_Report_AAPL.html delete mode 100644 Backtest_Report_TSLA.html delete mode 100644 Backtest_Trades_AAPL.xlsx delete mode 100644 Backtest_Trades_TSLA.xlsx create mode 100644 batch_optimizer.py delete mode 100644 markt_scan_20260121.xlsx create mode 100644 optimizers.py create mode 100644 strategies.py create mode 100644 tickers.json create mode 100644 tickers1.json diff --git a/.gitignore b/.gitignore index 36b13f1..9e45c54 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ + +output/ + # ---> Python # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/Backtest_Report_AAPL.html b/Backtest_Report_AAPL.html deleted file mode 100644 index f9686a8..0000000 --- a/Backtest_Report_AAPL.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - - Backtest_Report_AAPL.html - - - - - -
- - - - - \ No newline at end of file diff --git a/Backtest_Report_TSLA.html b/Backtest_Report_TSLA.html deleted file mode 100644 index c3be272..0000000 --- a/Backtest_Report_TSLA.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - - Backtest_Report_TSLA.html - - - - - -
- - - - - \ No newline at end of file diff --git a/Backtest_Trades_AAPL.xlsx b/Backtest_Trades_AAPL.xlsx deleted file mode 100644 index 45be3f30a2243bc645c98471d32c7999e8427712..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5361 zcmZ`-1yodP*B%h01qP5VDFcQYq}!oWiJ?"uX|4yC1KNTsAvYCsyKQ)(zl2?=2? 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 diff --git a/backtester.py b/backtester.py index f98b3e1..fa33d00 100644 --- a/backtester.py +++ b/backtester.py @@ -1,4 +1,6 @@ import os +import csv +import argparse import pandas as pd import pandas_ta as ta from dotenv import load_dotenv @@ -18,6 +20,10 @@ load_dotenv() API_KEY = os.getenv('ALPACA_API_KEY') SECRET_KEY = os.getenv('ALPACA_SECRET_KEY') +PATH_TO_OUTPUT = "output/" + +# Ensure output folder exists +os.makedirs(PATH_TO_OUTPUT, exist_ok=True) # --- KLASSE: DATEN-ENGINE --- class DataEngine: @staticmethod @@ -27,7 +33,7 @@ class DataEngine: request = StockBarsRequest( symbol_or_symbols=[symbol], - timeframe=TimeFrame.Day, + timeframe=TimeFrame.Hour, start=start_date ) @@ -43,18 +49,52 @@ class DataEngine: # --- KLASSE: STRATEGIE (RSI) --- class MyRsiStrategy(Strategy): # Parameter - diese können später optimiert werden - rsi_period = 25 - rsi_low = 30 - rsi_high = 70 + rsi_period = 29 + rsi_low = 35 + rsi_high = 75 + # Position sizing: fraction of equity to allocate per new trade (0-1) + position_size_pct = 0.1 + # Fallback fixed minimum shares to buy if computed shares is 0 + min_shares = 1 + # ATR-based stop-loss parameters + atr_period = 18 + # Stop-loss multiplier applied to ATR (e.g. 3 -> stop = entry - 3 * ATR) + stop_loss_atr_multiplier = 4.0 def init(self): # Indikator berechnen (self.I stellt sicher, dass er im Chart erscheint) self.rsi = self.I(ta.rsi, pd.Series(self.data.Close), length=self.rsi_period) + # Average True Range for volatility-based stops + self.atr = self.I( + ta.atr, + pd.Series(self.data.High), + pd.Series(self.data.Low), + pd.Series(self.data.Close), + length=self.atr_period, + ) def next(self): # KAUFEN: Wenn RSI die untere Grenze von unten nach oben kreuzt if crossover(self.rsi, self.rsi_low): - self.buy() + price = float(self.data.Close[-1]) + # Use a conservative fraction of current equity to size the position + max_invest = max(0, self.equity * self.position_size_pct) + shares = int(max_invest // price) + if shares < self.min_shares: + shares = self.min_shares + # Compute ATR-based stop-loss price below entry + atr_value = float(self.atr[-1]) if not pd.isna(self.atr[-1]) else None + if atr_value and atr_value > 0: + stop_price = round(price - (self.stop_loss_atr_multiplier * atr_value), 6) + # Ensure stop is below price + if stop_price >= price: + stop_price = round(price * 0.99, 6) + else: + # Fallback to a small percent-based stop if ATR not ready + stop_price = round(price * 0.98, 6) + + # Place an integer-sized buy order with SL (avoids relative-size cancellation) + self.buy(size=shares, sl=stop_price) # VERKAUFEN: Wenn RSI die obere Grenze von oben nach unten kreuzt elif crossover(self.rsi_high, self.rsi): @@ -62,18 +102,17 @@ class MyRsiStrategy(Strategy): self.position.close() # --- HAUPTPROGRAMM --- -def run_backtest(symbol="AAPL", cash=10000, commission=0.002): +def run_backtest(symbol="AAPL", days=365, cash=1000, commission=0.002): # 1. Daten laden print(f"Lade Daten für {symbol}...") - data = DataEngine.get_alpaca_data(symbol) + data = DataEngine.get_alpaca_data(symbol, days=days) # 2. Backtest initialisieren - # commission=0.002 bedeutet 0.2% Gebühren pro Trade - bt = Backtest(data, MyRsiStrategy, cash=cash, commission=commission) + bt = Backtest(data, MyRsiStrategy, cash=cash, commission=commission, finalize_trades=True) # 3. Backtest ausführen stats = bt.run() - + # --- AUSWERTUNG --- print("\n" + "="*30) print(f"BACKTEST ERGEBNISSE FÜR {symbol}") @@ -86,20 +125,85 @@ def run_backtest(symbol="AAPL", cash=10000, commission=0.002): print(f"Win Rate [%]: {stats['Win Rate [%]']:.2f}%") print("="*30) + result = { + 'symbol': symbol, + 'start_cash': cash, + 'end_equity': stats.get('Equity Final [$]', None), + 'return_pct': stats.get('Return [%]', None), + 'max_drawdown_pct': stats.get('Max. Drawdown [%]', None), + 'n_trades': stats.get('# Trades', None), + 'win_rate_pct': stats.get('Win Rate [%]', None), + } + # 4. EXPORT: Trades nach Excel - trades = stats['_trades'] - if not trades.empty: - # Dauer der Trades berechnen + trades = stats.get('_trades') + if trades is not None and not trades.empty: trades['Duration'] = trades['ExitTime'] - trades['EntryTime'] - excel_name = f"Backtest_Trades_{symbol}.xlsx" + excel_name = os.path.join(PATH_TO_OUTPUT, f"Backtest_Trades_{symbol}.xlsx") trades.to_excel(excel_name) print(f"✅ Excel-Liste gespeichert: {excel_name}") + result['trades_file'] = excel_name # 5. EXPORT: Interaktiver HTML Report - report_name = f"Backtest_Report_{symbol}.html" + report_name = os.path.join(PATH_TO_OUTPUT, f"Backtest_Report_{symbol}.html") bt.plot(filename=report_name, open_browser=False) print(f"✅ Interaktiver Chart gespeichert: {report_name}") + result['report_file'] = report_name + + return result + + +def run_backtests(symbols, days=365, cash=1000, commission=0.002, summary_name=None): + summary = [] + for sym in symbols: + try: + res = run_backtest(symbol=sym, days=days, cash=cash, commission=commission) + summary.append(res) + except Exception as e: + print(f"⚠️ Fehler beim Backtest für {sym}: {e}") + summary.append({'symbol': sym, 'error': str(e)}) + + # Write summary CSV + if summary_name is None: + ts = datetime.now().strftime('%Y%m%d_%H%M%S') + summary_name = os.path.join(PATH_TO_OUTPUT, f"backtest_summary_{ts}.csv") + + keys = set() + for row in summary: + keys.update(row.keys()) + keys = list(keys) + + with open(summary_name, 'w', newline='', encoding='utf-8') as csvfile: + writer = csv.DictWriter(csvfile, fieldnames=keys) + writer.writeheader() + for row in summary: + writer.writerow(row) + + print(f"\n✅ Zusammenfassung gespeichert: {summary_name}") + return summary_name if __name__ == "__main__": - # Starte den Backtest - run_backtest(symbol="AAPL", cash=10000) \ No newline at end of file + parser = argparse.ArgumentParser(description='Batch backtester for multiple stocks') + parser.add_argument('--tickers', type=str, help='Comma-separated list of tickers, e.g. AAPL,MSFT,TSLA') + parser.add_argument('--file', type=str, help='Path to a file containing one ticker per line') + parser.add_argument('--days', type=int, default=365, help='Days of historical data to fetch') + parser.add_argument('--cash', type=float, default=1000, help='Starting cash per backtest') + parser.add_argument('--commission', type=float, default=0.002, help='Commission (fraction), e.g. 0.002') + args = parser.parse_args() + + symbols = [] + if args.tickers: + symbols = [s.strip().upper() for s in args.tickers.split(',') if s.strip()] + elif args.file: + if os.path.exists(args.file): + with open(args.file, 'r', encoding='utf-8') as f: + symbols = [line.strip().upper() for line in f if line.strip()] + else: + print(f"Ticker-Datei nicht gefunden: {args.file}") + raise SystemExit(1) + else: + symbols = ["AAPL"] + + print(f"Starte Batch-Backtest für: {symbols}") + summary_csv = run_backtests(symbols, days=args.days, cash=args.cash, commission=args.commission) + print(f"Fertig. Übersicht: {summary_csv}") \ No newline at end of file diff --git a/batch_optimizer.py b/batch_optimizer.py new file mode 100644 index 0000000..1e7c0f9 --- /dev/null +++ b/batch_optimizer.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +import argparse +import json +import os +import concurrent.futures +from optimizer import run_optimized_backtest +import optimizers + + +def load_symbols_from_file(path): + with open(path, 'r', encoding='utf-8') as f: + return [line.strip().upper() for line in f if line.strip()] + + +def main(): + parser = argparse.ArgumentParser(description='Run optimizer for multiple tickers') + parser.add_argument('--tickers', type=str, help='Comma-separated list of tickers, e.g. AAPL,MSFT') + parser.add_argument('--file', type=str, help='Path to a file containing one ticker per line') + parser.add_argument('--symbols', nargs='*', help='Space-separated tickers') + parser.add_argument('--json', type=str, help='Path to a JSON file with either an array of tickers or an object with a "tickers" key') + parser.add_argument('--workers', type=int, default=1, help='Number of parallel workers (processes) to use') + parser.add_argument('--optimizers', type=str, help='Comma-separated optimizer names from optimizers.py, e.g. RsiOptimizer') + args = parser.parse_args() + + symbols = [] + if args.tickers: + symbols = [s.strip().upper() for s in args.tickers.split(',') if s.strip()] + elif args.file: + if os.path.exists(args.file): + symbols = load_symbols_from_file(args.file) + else: + print(f"Ticker file not found: {args.file}") + raise SystemExit(1) + elif args.json: + if os.path.exists(args.json): + with open(args.json, 'r', encoding='utf-8') as f: + data = json.load(f) + if isinstance(data, list): + symbols = [s.strip().upper() for s in data if isinstance(s, str) and s.strip()] + elif isinstance(data, dict): + # support {'tickers': [...]} or {'symbols': [...]} keys + arr = data.get('tickers') or data.get('symbols') + if isinstance(arr, list): + symbols = [s.strip().upper() for s in arr if isinstance(s, str) and s.strip()] + else: + print(f"JSON file does not contain a list under 'tickers' or 'symbols': {args.json}") + raise SystemExit(1) + else: + print(f"JSON root must be an array or object: {args.json}") + raise SystemExit(1) + else: + print(f"JSON file not found: {args.json}") + raise SystemExit(1) + elif args.symbols: + symbols = [s.upper() for s in args.symbols] + else: + parser.print_help() + raise SystemExit(1) + + # Resolve optimizers + if args.optimizers: + optimizer_names = [s.strip() for s in args.optimizers.split(',') if s.strip()] + else: + optimizer_names = ['RsiOptimizer'] + + # Build job list (symbol, optimizer_name) pairs + jobs = [(sym, opt_name) for sym in symbols for opt_name in optimizer_names] + + workers = max(1, int(args.workers or 1)) + results = [] + + if workers > 1: + print(f"Running optimizations in parallel with {workers} workers...") + with concurrent.futures.ProcessPoolExecutor(max_workers=workers) as executor: + # Use module-level runner to ensure picklable callable + future_to_job = {executor.submit(optimizers.run_optimizer, sym, opt_name): (sym, opt_name) for (sym, opt_name) in jobs} + for fut in concurrent.futures.as_completed(future_to_job): + sym, opt_name = future_to_job[fut] + try: + res = fut.result() + if isinstance(res, dict): + # tag result with optimizer name + res['optimizer'] = opt_name + results.append(res) + except Exception as e: + print(f"Error optimizing {sym} with {opt_name}: {e}") + else: + for sym, opt_name in jobs: + try: + print(f"\n--- Optimizing {sym} using {opt_name} ---") + res = optimizers.run_optimizer(sym, opt_name) + if isinstance(res, dict): + res['optimizer'] = opt_name + results.append(res) + except Exception as e: + print(f"Error optimizing {sym} with {opt_name}: {e}") + + # Save results to Excel + if results: + try: + import pandas as pd + from datetime import datetime + + ts = datetime.now().strftime('%Y%m%d_%H%M%S') + out_name = os.path.join('output', f'optimized_summary_{ts}.xlsx') + df = pd.DataFrame(results) + os.makedirs('output', exist_ok=True) + df.to_excel(out_name, index=False) + print(f"\n✅ Optimized summary saved: {out_name}") + except Exception as e: + print(f"Failed to write Excel summary: {e}") + + +if __name__ == '__main__': + main() diff --git a/checkStocks.py b/checkStocks.py index 4f4564f..3878246 100644 --- a/checkStocks.py +++ b/checkStocks.py @@ -63,7 +63,7 @@ def scan_markets(tickers, rsi_period=25): # Als Excel speichern filename = f"markt_scan_{datetime.now().strftime('%Y%m%d')}.xlsx" - scan_df.to_excel(filename, index=False) + scan_df.to_excel("output/"+filename, index=False) print(f"\n✅ Scan abgeschlossen. Ergebnisse gespeichert in: {filename}") return scan_df diff --git a/markt_scan_20260121.xlsx b/markt_scan_20260121.xlsx deleted file mode 100644 index 21a06933bc9c20d3ceb178f13daca74471df7ba9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5219 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 XmZmBOa=-)t;2=+_Um2YA*W3RAi5(w^ diff --git a/optimizer.py b/optimizer.py index a30a978..4c81ebf 100644 --- a/optimizer.py +++ b/optimizer.py @@ -4,8 +4,8 @@ import pandas_ta as ta from dotenv import load_dotenv from datetime import datetime, timedelta -from backtesting import Backtest, Strategy -from backtesting.lib import crossover +from backtesting import Backtest +from strategies import RsiStrategy from alpaca.data.historical import StockHistoricalDataClient from alpaca.data.requests import StockBarsRequest from alpaca.data.timeframe import TimeFrame @@ -14,60 +14,99 @@ from alpaca.data.timeframe import TimeFrame load_dotenv() API_KEY = os.getenv('ALPACA_API_KEY') SECRET_KEY = os.getenv('ALPACA_SECRET_KEY') +PATH_TO_OUTPUT = "output/" +os.makedirs(PATH_TO_OUTPUT, exist_ok=True) def get_data(symbol, days=365): client = StockHistoricalDataClient(API_KEY, SECRET_KEY) start_date = datetime.now() - timedelta(days=days) - request_params = StockBarsRequest(symbol_or_symbols=[symbol], timeframe=TimeFrame.Day, start=start_date) + request_params = StockBarsRequest(symbol_or_symbols=[symbol], timeframe=TimeFrame.Hour, start=start_date) df = client.get_stock_bars(request_params).df df = df.reset_index(level=0, drop=True) df.columns = [c.capitalize() for c in df.columns] df.index = df.index.tz_localize(None) return df -# --- 2. STRATEGIE MIT OPTIMIERBAREN PARAMETERN --- -class RsiStrategy(Strategy): - # Diese Klassenvariablen werden von der Optimize-Funktion überschrieben - rsi_period = 14 - rsi_lower = 30 - rsi_upper = 70 - - def init(self): - # Wichtig: Wir übergeben die Parameter an pandas_ta - self.rsi = self.I(ta.rsi, pd.Series(self.data.Close), length=self.rsi_period) - - def next(self): - if crossover(self.rsi, self.rsi_lower): - self.buy() - elif crossover(self.rsi_upper, self.rsi): - if self.position: - self.position.close() +# Strategy classes moved to strategies.py # --- 3. OPTIMIERUNGS-ENGINE --- -def run_optimized_backtest(symbol): +def run_optimized_backtest(symbol, strategy_cls=RsiStrategy, optimize_kwargs=None, report_tag=None): data = get_data(symbol) - bt = Backtest(data, RsiStrategy, cash=10000, commission=0.001) + bt = Backtest(data, strategy_cls, cash=10000, commission=0.001, finalize_trades=True) - print(f"--- Starte Optimierung für {symbol} ---") - - # Hier passiert die Magie: - stats = bt.optimize( - rsi_period=range(7, 30, 2), # Teste Perioden von 7 bis 29 in 2er Schritten - rsi_lower=range(20, 40, 5), # Teste Kaufsignale von 20 bis 35 - rsi_upper=range(60, 80, 5), # Teste Verkaufsignale von 60 bis 75 - maximize='Return [%]', # Wir wollen den höchsten Gewinn (oder 'Sharpe Ratio') - constraint=lambda p: p.rsi_upper > p.rsi_lower # Logik-Check - ) + print(f"--- Starte Optimierung für {symbol} using {strategy_cls.__name__} ---") + + # Build common optimization params for strategy if not provided + if optimize_kwargs is None: + optimize_kwargs = dict( + rsi_period=range(7, 30, 2), # Teste Perioden von 7 bis 29 in 2er Schritten + rsi_lower=range(20, 40, 5), # Teste Kaufsignale von 20 bis 35 + rsi_upper=range(60, 80, 5), # Teste Verkaufsignale von 60 bis 75 + maximize='Return [%]', # Wir wollen den höchsten Gewinn (oder 'Sharpe Ratio') + constraint=lambda p: p.rsi_upper > p.rsi_lower, # Logik-Check + ) + # Extend with ATR/stop params if strategy supports them + if hasattr(strategy_cls, 'atr_period'): + optimize_kwargs['atr_period'] = range(10, 20, 2) + if hasattr(strategy_cls, 'stop_loss_atr_multiplier'): + optimize_kwargs['stop_loss_atr_multiplier'] = [2.0, 2.5, 3.0, 3.5, 4.0] + + # Run optimization + stats = bt.optimize(**optimize_kwargs) print("\n--- BESTE PARAMETER GEFUNDEN ---") print(stats) print("\nDetails der besten Strategie:") - print(f"RSI Periode: {stats._strategy.rsi_period}") - print(f"RSI Untergrenze: {stats._strategy.rsi_lower}") - print(f"RSI Obergrenze: {stats._strategy.rsi_upper}") - + # Print only attributes that the strategy actually has + for attr in ('rsi_period', 'rsi_lower', 'rsi_upper', 'short_ema', 'long_ema', 'atr_period', 'stop_loss_atr_multiplier'): + if hasattr(stats._strategy, attr): + print(f"{attr.replace('_', ' ').capitalize()}: {getattr(stats._strategy, attr)}") # Speichere den Chart der besten Strategie - bt.plot(filename=f"optimized_report_{symbol}.html", open_browser=False) + tag = f"_{report_tag}" if report_tag else "" + out_path = os.path.join(PATH_TO_OUTPUT, f"optimized_report_{symbol}{tag}.html") + bt.plot(filename=out_path, open_browser=False) + print(f"Optimized report saved: {out_path}") + + # Build a result dict to return to callers + result = { + 'symbol': symbol, + 'rsi_period': getattr(stats._strategy, 'rsi_period', None), + 'rsi_lower': getattr(stats._strategy, 'rsi_lower', None), + 'rsi_upper': getattr(stats._strategy, 'rsi_upper', None), + 'short_ema': getattr(stats._strategy, 'short_ema', None), + 'long_ema': getattr(stats._strategy, 'long_ema', None), + 'atr_period': getattr(stats._strategy, 'atr_period', None), + 'stop_loss_atr_multiplier': getattr(stats._strategy, 'stop_loss_atr_multiplier', None), + 'return_pct': stats.get('Return [%]') if hasattr(stats, 'get') else None, + 'equity_final_$': stats.get('Equity Final [$]') if hasattr(stats, 'get') else None, + 'max_drawdown_pct': stats.get('Max. Drawdown [%]') if hasattr(stats, 'get') else None, + 'n_trades': stats.get('# Trades') if hasattr(stats, 'get') else None, + 'win_rate_pct': stats.get('Win Rate [%]') if hasattr(stats, 'get') else None, + } + + # Run a final backtest with the best-found parameters to export the full trades list + best_params = {} + for attr in ('rsi_period', 'rsi_lower', 'rsi_upper', 'short_ema', 'long_ema', 'atr_period', 'stop_loss_atr_multiplier'): + if hasattr(stats._strategy, attr): + best_params[attr] = getattr(stats._strategy, attr) + + try: + if best_params: + print(f"Running final backtest for {symbol} with best params: {best_params}") + final_stats = bt.run(**best_params) + trades = final_stats.get('_trades') + if trades is not None and not trades.empty: + # add duration column and export + trades['Duration'] = trades['ExitTime'] - trades['EntryTime'] + tag_name = report_tag if report_tag else strategy_cls.__name__ + trades_file = os.path.join(PATH_TO_OUTPUT, f"Backtest_Trades_{symbol}_{tag_name}.xlsx") + trades.to_excel(trades_file, index=False) + print(f"✅ Trades exported: {trades_file}") + result['trades_file'] = trades_file + except Exception as e: + print(f"Failed to run final backtest for trades export: {e}") + + return result if __name__ == "__main__": - run_optimized_backtest("AAPL") \ No newline at end of file + run_optimized_backtest("GOLD") \ No newline at end of file diff --git a/optimizers.py b/optimizers.py new file mode 100644 index 0000000..d2c5869 --- /dev/null +++ b/optimizers.py @@ -0,0 +1,69 @@ +import os +from typing import Dict, Any +import strategies +from optimizer import run_optimized_backtest + + +class Optimizer: + """Base optimizer wrapper tying a strategy to an optimization grid.""" + name = 'BaseOptimizer' + strategy_cls = None + optimize_kwargs: Dict[str, Any] = None + + @classmethod + def run(cls, symbol): + # pass optimizer name as report_tag so output files are unique per optimizer + return run_optimized_backtest(symbol, strategy_cls=cls.strategy_cls, optimize_kwargs=cls.optimize_kwargs, report_tag=cls.name) + + +class RsiOptimizer(Optimizer): + name = 'RsiOptimizer' + strategy_cls = strategies.RsiStrategy + def constraint(p): + return p.rsi_upper > p.rsi_lower + + optimize_kwargs = dict( + rsi_period=range(7, 30, 2), + rsi_lower=range(20, 40, 5), + rsi_upper=range(60, 80, 5), + maximize='Return [%]', + constraint=constraint, + atr_period=range(10, 20, 2), + stop_loss_atr_multiplier=[2.0, 2.5, 3.0, 3.5, 4.0], + ) + + +class CrossEmaOptimizer(Optimizer): + name = 'CrossEmaOptimizer' + strategy_cls = strategies.CrossEmaStrategy + + def constraint(p): + # ensure long EMA period is greater than short EMA period + return p.long_ema > p.short_ema + + optimize_kwargs = dict( + short_ema=range(5, 20, 3), + long_ema=range(20, 60, 5), + maximize='Return [%]', + constraint=constraint, + atr_period=range(10, 20, 2), + stop_loss_atr_multiplier=[2.0, 2.5, 3.0, 3.5], + finalize_trades=False, # run final backtest with best params + ) + + +def get_optimizer_by_name(name: str): + mapping = {cls.name: cls for cls in (RsiOptimizer, CrossEmaOptimizer)} + # allow passing class name as well + if name in mapping: + return mapping[name] + # try attribute lookup in module + cls = getattr(__import__('optimizers'), name, None) + return cls + + +def run_optimizer(symbol: str, optimizer_name: str): + cls = get_optimizer_by_name(optimizer_name) + if cls is None: + raise ValueError(f"Optimizer '{optimizer_name}' not found") + return cls.run(symbol) diff --git a/strategies.py b/strategies.py new file mode 100644 index 0000000..be5189d --- /dev/null +++ b/strategies.py @@ -0,0 +1,92 @@ +import pandas as pd +import pandas_ta as ta +from backtesting import Strategy +from backtesting.lib import crossover + + +class RsiStrategy(Strategy): + # Default parameters (can be overridden by optimizer) + rsi_period = 14 + rsi_lower = 30 + rsi_upper = 70 + # Position sizing and ATR stop parameters + position_size_pct = 0.1 + min_shares = 1 + atr_period = 14 + stop_loss_atr_multiplier = 3.0 + + def init(self): + self.rsi = self.I(ta.rsi, pd.Series(self.data.Close), length=self.rsi_period) + self.atr = self.I( + ta.atr, + pd.Series(self.data.High), + pd.Series(self.data.Low), + pd.Series(self.data.Close), + length=self.atr_period, + ) + + def next(self): + if crossover(self.rsi, self.rsi_lower): + price = float(self.data.Close[-1]) + max_invest = max(0, self.equity * self.position_size_pct) + shares = int(max_invest // price) + if shares < self.min_shares: + shares = self.min_shares + + atr_value = float(self.atr[-1]) if not pd.isna(self.atr[-1]) else None + if atr_value and atr_value > 0: + stop_price = round(price - (self.stop_loss_atr_multiplier * atr_value), 6) + if stop_price >= price: + stop_price = round(price * 0.99, 6) + else: + stop_price = round(price * 0.98, 6) + + self.buy(size=shares, sl=stop_price) + elif crossover(self.rsi_upper, self.rsi): + if self.position: + self.position.close() + + +class CrossEmaStrategy(Strategy): + """EMA crossover strategy with ATR-based stop-loss.""" + short_ema = 12 + long_ema = 26 + position_size_pct = 0.1 + min_shares = 1 + atr_period = 14 + stop_loss_atr_multiplier = 3.0 + + def init(self): + self.ema_short = self.I(ta.ema, pd.Series(self.data.Close), length=self.short_ema) + self.ema_long = self.I(ta.ema, pd.Series(self.data.Close), length=self.long_ema) + self.atr = self.I( + ta.atr, + pd.Series(self.data.High), + pd.Series(self.data.Low), + pd.Series(self.data.Close), + length=self.atr_period, + ) + + def next(self): + price = float(self.data.Close[-1]) + # Buy when short EMA crosses above long EMA + if crossover(self.ema_short, self.ema_long): + max_invest = max(0, self.equity * self.position_size_pct) + shares = int(max_invest // price) + if shares < self.min_shares: + shares = self.min_shares + + atr_value = float(self.atr[-1]) if not pd.isna(self.atr[-1]) else None + if atr_value and atr_value > 0: + stop_price = round(price - (self.stop_loss_atr_multiplier * atr_value), 6) + if stop_price >= price: + stop_price = round(price * 0.99, 6) + else: + stop_price = round(price * 0.98, 6) + + self.buy(size=shares, sl=stop_price) + + # Sell when short EMA crosses below long EMA + elif crossover(self.ema_long, self.ema_short): + if self.position: + self.position.close() diff --git a/tickers.json b/tickers.json new file mode 100644 index 0000000..c67c417 --- /dev/null +++ b/tickers.json @@ -0,0 +1,6 @@ +[ + "AAPL", + "MSFT", + "TSLA", + "GOOG" +] \ No newline at end of file diff --git a/tickers1.json b/tickers1.json new file mode 100644 index 0000000..159ce24 --- /dev/null +++ b/tickers1.json @@ -0,0 +1,19 @@ +[ + "AAPL", + "MSFT", + "TSLA", + "GOOG", + "AMZN", + "GOLD", + "NFLX", + "META", + "NVDA", + "BRK.B", + "JPM", + "V", + "DIS", + "PYPL", + "ADBE", + "INTC", + "CMCSA" +]