Remove deprecated modules and files related to the backtesting framework, including backtest.py, cli.py, config.py, data.py, intrabar.py, logging_utils.py, market_costs.py, metrics.py, trade.py, and supertrend indicators. Introduce a new structure for the backtesting engine with improved organization and functionality, including a CLI handler, data manager, and reporting capabilities. Update dependencies in pyproject.toml to support the new architecture.
This commit is contained in:
243
engine/cli.py
Normal file
243
engine/cli.py
Normal file
@@ -0,0 +1,243 @@
|
||||
"""
|
||||
CLI handler for Lowkey Backtest.
|
||||
"""
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from engine.backtester import Backtester
|
||||
from engine.data_manager import DataManager
|
||||
from engine.logging_config import get_logger, setup_logging
|
||||
from engine.market import MarketType
|
||||
from engine.reporting import Reporter
|
||||
from strategies.factory import get_strategy, get_strategy_names
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def create_parser() -> argparse.ArgumentParser:
|
||||
"""Create and configure the argument parser."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Lowkey Backtest CLI (VectorBT Edition)"
|
||||
)
|
||||
subparsers = parser.add_subparsers(dest="command", help="Command to run")
|
||||
|
||||
_add_download_parser(subparsers)
|
||||
_add_backtest_parser(subparsers)
|
||||
_add_wfa_parser(subparsers)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def _add_download_parser(subparsers) -> None:
|
||||
"""Add download command parser."""
|
||||
dl_parser = subparsers.add_parser("download", help="Download historical data")
|
||||
dl_parser.add_argument("--exchange", "-e", type=str, default="okx")
|
||||
dl_parser.add_argument("--pair", "-p", type=str, required=True)
|
||||
dl_parser.add_argument("--timeframe", "-t", type=str, default="1m")
|
||||
dl_parser.add_argument("--start", type=str, help="Start Date (YYYY-MM-DD)")
|
||||
dl_parser.add_argument(
|
||||
"--market", "-m",
|
||||
type=str,
|
||||
choices=["spot", "perpetual"],
|
||||
default="spot"
|
||||
)
|
||||
|
||||
|
||||
def _add_backtest_parser(subparsers) -> None:
|
||||
"""Add backtest command parser."""
|
||||
strategy_choices = get_strategy_names()
|
||||
|
||||
bt_parser = subparsers.add_parser("backtest", help="Run a backtest")
|
||||
bt_parser.add_argument(
|
||||
"--strategy", "-s",
|
||||
type=str,
|
||||
choices=strategy_choices,
|
||||
required=True
|
||||
)
|
||||
bt_parser.add_argument("--exchange", "-e", type=str, default="okx")
|
||||
bt_parser.add_argument("--pair", "-p", type=str, required=True)
|
||||
bt_parser.add_argument("--timeframe", "-t", type=str, default="1m")
|
||||
bt_parser.add_argument("--start", type=str)
|
||||
bt_parser.add_argument("--end", type=str)
|
||||
bt_parser.add_argument("--grid", "-g", action="store_true")
|
||||
bt_parser.add_argument("--plot", action="store_true")
|
||||
|
||||
# Risk parameters
|
||||
bt_parser.add_argument("--sl", type=float, help="Stop Loss %%")
|
||||
bt_parser.add_argument("--tp", type=float, help="Take Profit %%")
|
||||
bt_parser.add_argument("--trail", action="store_true")
|
||||
bt_parser.add_argument("--no-bear-exit", action="store_true")
|
||||
|
||||
# Cost parameters
|
||||
bt_parser.add_argument("--fees", type=float, default=None)
|
||||
bt_parser.add_argument("--slippage", type=float, default=0.001)
|
||||
bt_parser.add_argument("--leverage", "-l", type=int, default=None)
|
||||
|
||||
|
||||
def _add_wfa_parser(subparsers) -> None:
|
||||
"""Add walk-forward analysis command parser."""
|
||||
strategy_choices = get_strategy_names()
|
||||
|
||||
wfa_parser = subparsers.add_parser("wfa", help="Run Walk-Forward Analysis")
|
||||
wfa_parser.add_argument(
|
||||
"--strategy", "-s",
|
||||
type=str,
|
||||
choices=strategy_choices,
|
||||
required=True
|
||||
)
|
||||
wfa_parser.add_argument("--pair", "-p", type=str, required=True)
|
||||
wfa_parser.add_argument("--timeframe", "-t", type=str, default="1d")
|
||||
wfa_parser.add_argument("--windows", "-w", type=int, default=10)
|
||||
wfa_parser.add_argument("--plot", action="store_true")
|
||||
|
||||
|
||||
def run_download(args) -> None:
|
||||
"""Execute download command."""
|
||||
dm = DataManager()
|
||||
market_type = MarketType(args.market)
|
||||
dm.download_data(
|
||||
args.exchange,
|
||||
args.pair,
|
||||
args.timeframe,
|
||||
start_date=args.start,
|
||||
market_type=market_type
|
||||
)
|
||||
|
||||
|
||||
def run_backtest(args) -> None:
|
||||
"""Execute backtest command."""
|
||||
dm = DataManager()
|
||||
bt = Backtester(dm)
|
||||
reporter = Reporter()
|
||||
|
||||
strategy, params = get_strategy(args.strategy, args.grid)
|
||||
|
||||
# Apply CLI overrides for meta_st strategy
|
||||
params = _apply_strategy_overrides(args, strategy, params)
|
||||
|
||||
if args.grid and args.strategy == "meta_st":
|
||||
logger.info("Running Grid Search for Meta Supertrend...")
|
||||
|
||||
try:
|
||||
result = bt.run_strategy(
|
||||
strategy,
|
||||
args.exchange,
|
||||
args.pair,
|
||||
timeframe=args.timeframe,
|
||||
start_date=args.start,
|
||||
end_date=args.end,
|
||||
fees=args.fees,
|
||||
slippage=args.slippage,
|
||||
sl_stop=args.sl,
|
||||
tp_stop=args.tp,
|
||||
sl_trail=args.trail,
|
||||
leverage=args.leverage,
|
||||
**params
|
||||
)
|
||||
|
||||
reporter.print_summary(result)
|
||||
reporter.save_reports(result, f"{args.strategy}_{args.pair.replace('/','-')}")
|
||||
|
||||
if args.plot and not args.grid:
|
||||
reporter.plot(result.portfolio)
|
||||
elif args.plot and args.grid:
|
||||
logger.info("Plotting skipped for Grid Search. Check CSV results.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Backtest failed: %s", e, exc_info=True)
|
||||
|
||||
|
||||
def run_wfa(args) -> None:
|
||||
"""Execute walk-forward analysis command."""
|
||||
dm = DataManager()
|
||||
bt = Backtester(dm)
|
||||
reporter = Reporter()
|
||||
|
||||
strategy, params = get_strategy(args.strategy, is_grid=True)
|
||||
|
||||
logger.info(
|
||||
"Running WFA on %s for %s (%s) with %d windows...",
|
||||
args.strategy, args.pair, args.timeframe, args.windows
|
||||
)
|
||||
|
||||
try:
|
||||
results, stitched_curve = bt.run_wfa(
|
||||
strategy,
|
||||
"okx",
|
||||
args.pair,
|
||||
params,
|
||||
n_windows=args.windows,
|
||||
timeframe=args.timeframe
|
||||
)
|
||||
|
||||
_log_wfa_results(results)
|
||||
_save_wfa_results(args, results, stitched_curve, reporter)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("WFA failed: %s", e, exc_info=True)
|
||||
|
||||
|
||||
def _apply_strategy_overrides(args, strategy, params: dict) -> dict:
|
||||
"""Apply CLI argument overrides to strategy parameters."""
|
||||
if args.strategy != "meta_st":
|
||||
return params
|
||||
|
||||
if args.no_bear_exit:
|
||||
params['exit_on_bearish_flip'] = False
|
||||
|
||||
if args.sl is None:
|
||||
args.sl = strategy.default_sl_stop
|
||||
|
||||
if not args.trail:
|
||||
args.trail = strategy.default_sl_trail
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def _log_wfa_results(results) -> None:
|
||||
"""Log WFA results summary."""
|
||||
logger.info("Walk-Forward Analysis Results:")
|
||||
|
||||
if results.empty or 'window' not in results.columns:
|
||||
logger.warning("No valid WFA results. All windows may have failed.")
|
||||
return
|
||||
|
||||
columns = ['window', 'train_score', 'test_score', 'test_return']
|
||||
logger.info("\n%s", results[columns].to_string(index=False))
|
||||
|
||||
avg_test_sharpe = results['test_score'].mean()
|
||||
avg_test_return = results['test_return'].mean()
|
||||
logger.info("Average Test Sharpe: %.2f", avg_test_sharpe)
|
||||
logger.info("Average Test Return: %.2f%%", avg_test_return * 100)
|
||||
|
||||
|
||||
def _save_wfa_results(args, results, stitched_curve, reporter) -> None:
|
||||
"""Save WFA results to file and optionally plot."""
|
||||
if results.empty:
|
||||
return
|
||||
|
||||
output_path = f"backtest_logs/wfa_{args.strategy}_{args.pair.replace('/','-')}.csv"
|
||||
results.to_csv(output_path)
|
||||
logger.info("Saved full results to %s", output_path)
|
||||
|
||||
if args.plot:
|
||||
reporter.plot_wfa(results, stitched_curve)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
setup_logging()
|
||||
|
||||
parser = create_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
commands = {
|
||||
"download": run_download,
|
||||
"backtest": run_backtest,
|
||||
"wfa": run_wfa,
|
||||
}
|
||||
|
||||
if args.command in commands:
|
||||
commands[args.command](args)
|
||||
else:
|
||||
parser.print_help()
|
||||
Reference in New Issue
Block a user