diff options
Diffstat (limited to 'services/backtester')
| -rw-r--r-- | services/backtester/Dockerfile | 3 | ||||
| -rw-r--r-- | services/backtester/pyproject.toml | 2 | ||||
| -rw-r--r-- | services/backtester/src/backtester/main.py | 51 |
3 files changed, 31 insertions, 25 deletions
diff --git a/services/backtester/Dockerfile b/services/backtester/Dockerfile index 77ec453..9a4f439 100644 --- a/services/backtester/Dockerfile +++ b/services/backtester/Dockerfile @@ -4,4 +4,7 @@ COPY shared/ shared/ RUN pip install --no-cache-dir ./shared COPY services/backtester/ services/backtester/ RUN pip install --no-cache-dir ./services/backtester +COPY services/strategy-engine/strategies/ /app/strategies/ +ENV STRATEGIES_DIR=/app/strategies +ENV PYTHONPATH=/app CMD ["python", "-m", "backtester.main"] diff --git a/services/backtester/pyproject.toml b/services/backtester/pyproject.toml index b51f913..2601d04 100644 --- a/services/backtester/pyproject.toml +++ b/services/backtester/pyproject.toml @@ -3,7 +3,7 @@ name = "backtester" version = "0.1.0" description = "Strategy backtesting engine" requires-python = ">=3.12" -dependencies = ["pandas>=2.0", "trading-shared"] +dependencies = ["pandas>=2.0", "numpy>=1.20", "rich>=13.0", "trading-shared"] [project.optional-dependencies] dev = ["pytest>=8.0", "pytest-asyncio>=0.23"] diff --git a/services/backtester/src/backtester/main.py b/services/backtester/src/backtester/main.py index c9b3890..0a2b47d 100644 --- a/services/backtester/src/backtester/main.py +++ b/services/backtester/src/backtester/main.py @@ -1,36 +1,41 @@ """Main entry point for the backtester service.""" - +import asyncio +import importlib import os import sys from decimal import Decimal +from pathlib import Path -from shared.db import Database # noqa: E402 -from shared.models import Candle # noqa: E402 - -from backtester.config import BacktestConfig # noqa: E402 -from backtester.engine import BacktestEngine # noqa: E402 -from backtester.reporter import format_report # noqa: E402 +# Resolve strategies directory from env or relative path +_STRATEGIES_DIR = Path(os.environ.get( + "STRATEGIES_DIR", + str(Path(__file__).resolve().parents[4] / "strategy-engine" / "strategies") +)) +if _STRATEGIES_DIR.parent not in [Path(p) for p in sys.path]: + sys.path.insert(0, str(_STRATEGIES_DIR.parent)) -# Allow importing strategies from the strategy-engine service -_STRATEGY_ENGINE_PATH = os.path.join(os.path.dirname(__file__), "../../../../strategy-engine") -if _STRATEGY_ENGINE_PATH not in sys.path: - sys.path.insert(0, _STRATEGY_ENGINE_PATH) +from shared.db import Database +from shared.models import Candle +from backtester.config import BacktestConfig +from backtester.engine import BacktestEngine +from backtester.reporter import format_report async def run_backtest() -> str: - """Load strategy, fetch candles, run backtest, and return a formatted report.""" config = BacktestConfig() - # Import strategy dynamically (requires strategy-engine in sys.path) try: - from strategies.base import BaseStrategy # noqa: F401 - - # Try to import concrete strategy by name - module_name = config.strategy_name - import importlib - - mod = importlib.import_module(f"strategies.{module_name}") - strategy_cls = getattr(mod, "Strategy") + mod = importlib.import_module(f"strategies.{config.strategy_name}") + # Find the first BaseStrategy subclass + from strategies.base import BaseStrategy + strategy_cls = None + for attr_name in dir(mod): + obj = getattr(mod, attr_name) + if isinstance(obj, type) and issubclass(obj, BaseStrategy) and obj is not BaseStrategy: + strategy_cls = obj + break + if strategy_cls is None: + raise RuntimeError(f"No strategy class found in {config.strategy_name}") strategy = strategy_cls() strategy.configure({}) except Exception as exc: @@ -41,7 +46,7 @@ async def run_backtest() -> str: try: rows = await db.get_candles(config.symbol, config.timeframe, config.candle_limit) candles = [Candle(**row) for row in rows] - candles = list(reversed(candles)) # oldest first for strategy processing + candles = list(reversed(candles)) finally: await db.close() @@ -51,7 +56,5 @@ async def run_backtest() -> str: if __name__ == "__main__": - import asyncio - report = asyncio.run(run_backtest()) print(report) |
