From 33b14aaa2344b0fd95d1629627c3d135b24ae102 Mon Sep 17 00:00:00 2001 From: TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:56:35 +0900 Subject: feat: initial trading platform implementation Binance spot crypto trading platform with microservices architecture: - shared: Pydantic models, Redis Streams broker, asyncpg DB layer - data-collector: Binance WebSocket/REST market data collection - strategy-engine: Plugin-based strategy execution (RSI, Grid) - order-executor: Order execution with risk management - portfolio-manager: Position tracking and PnL calculation - backtester: Historical strategy testing with simulator - cli: Click-based CLI for all operations - Docker Compose orchestration with Redis and PostgreSQL - 24 test files covering all modules --- services/backtester/tests/__init__.py | 0 services/backtester/tests/test_engine.py | 74 +++++++++++++++++++++++++++++ services/backtester/tests/test_reporter.py | 26 ++++++++++ services/backtester/tests/test_simulator.py | 73 ++++++++++++++++++++++++++++ 4 files changed, 173 insertions(+) create mode 100644 services/backtester/tests/__init__.py create mode 100644 services/backtester/tests/test_engine.py create mode 100644 services/backtester/tests/test_reporter.py create mode 100644 services/backtester/tests/test_simulator.py (limited to 'services/backtester/tests') diff --git a/services/backtester/tests/__init__.py b/services/backtester/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/backtester/tests/test_engine.py b/services/backtester/tests/test_engine.py new file mode 100644 index 0000000..1a25e1c --- /dev/null +++ b/services/backtester/tests/test_engine.py @@ -0,0 +1,74 @@ +"""Tests for the BacktestEngine.""" +from datetime import datetime, timezone +from decimal import Decimal +from unittest.mock import MagicMock + +import pytest + +from shared.models import Candle, Signal, OrderSide + +from backtester.engine import BacktestEngine, BacktestResult + + +def make_candle(symbol: str, price: float, timeframe: str = "1h") -> Candle: + return Candle( + symbol=symbol, + timeframe=timeframe, + open_time=datetime.now(timezone.utc), + open=Decimal(str(price)), + high=Decimal(str(price * 1.01)), + low=Decimal(str(price * 0.99)), + close=Decimal(str(price)), + volume=Decimal("100"), + ) + + +def make_candles(prices: list[float], symbol: str = "BTCUSDT") -> list[Candle]: + return [make_candle(symbol, p) for p in prices] + + +def make_signal(side: OrderSide, price: str, quantity: str = "0.1") -> Signal: + return Signal( + strategy="test", + symbol="BTCUSDT", + side=side, + price=Decimal(price), + quantity=Decimal(quantity), + reason="test", + ) + + +def test_backtest_engine_runs_strategy_over_candles(): + strategy = MagicMock() + strategy.name = "mock_strategy" + strategy.on_candle.return_value = None + + candles = make_candles([50000.0, 51000.0, 52000.0]) + engine = BacktestEngine(strategy, Decimal("10000")) + result = engine.run(candles) + + assert strategy.on_candle.call_count == 3 + assert result.total_trades == 0 + assert result.final_balance == Decimal("10000") + assert result.strategy_name == "mock_strategy" + + +def test_backtest_engine_executes_signals(): + buy_signal = make_signal(OrderSide.BUY, "50000", "0.1") + sell_signal = make_signal(OrderSide.SELL, "55000", "0.1") + + strategy = MagicMock() + strategy.name = "mock_strategy" + strategy.on_candle.side_effect = [buy_signal, None, sell_signal] + + candles = make_candles([50000.0, 52000.0, 55000.0]) + engine = BacktestEngine(strategy, Decimal("10000")) + result = engine.run(candles) + + assert result.total_trades == 2 + # Initial: 10000, bought 0.1 BTC @ 50000 (cost 5000) → balance 5000 + # Sold 0.1 BTC @ 55000 (proceeds 5500) → balance 10500 + expected_final = Decimal("10500") + assert result.final_balance == expected_final + expected_profit = Decimal("500") + assert result.profit == expected_profit diff --git a/services/backtester/tests/test_reporter.py b/services/backtester/tests/test_reporter.py new file mode 100644 index 0000000..f5c694c --- /dev/null +++ b/services/backtester/tests/test_reporter.py @@ -0,0 +1,26 @@ +"""Tests for the report formatter.""" +from decimal import Decimal + +from backtester.engine import BacktestResult +from backtester.reporter import format_report + + +def test_format_report_contains_key_metrics(): + result = BacktestResult( + strategy_name="sma_crossover", + symbol="BTCUSDT", + total_trades=10, + initial_balance=Decimal("10000"), + final_balance=Decimal("11500"), + profit=Decimal("1500"), + profit_pct=Decimal("15"), + trades=[], + ) + report = format_report(result) + + assert "sma_crossover" in report + assert "BTCUSDT" in report + assert "10000" in report + assert "11500" in report + assert "1500" in report + assert "15" in report diff --git a/services/backtester/tests/test_simulator.py b/services/backtester/tests/test_simulator.py new file mode 100644 index 0000000..9d8b23e --- /dev/null +++ b/services/backtester/tests/test_simulator.py @@ -0,0 +1,73 @@ +"""Tests for the OrderSimulator.""" +from decimal import Decimal + +import pytest + +from shared.models import Signal, OrderSide, OrderType +from backtester.simulator import OrderSimulator + + +def make_signal( + symbol: str, + side: OrderSide, + price: str, + quantity: str, + strategy: str = "test", +) -> Signal: + return Signal( + strategy=strategy, + symbol=symbol, + side=side, + price=Decimal(price), + quantity=Decimal(quantity), + reason="test", + ) + + +def test_simulator_initial_balance(): + sim = OrderSimulator(Decimal("10000")) + assert sim.balance == Decimal("10000") + + +def test_simulator_buy_reduces_balance(): + sim = OrderSimulator(Decimal("10000")) + signal = make_signal("BTCUSDT", OrderSide.BUY, "50000", "0.1") + result = sim.execute(signal) + assert result is True + assert sim.balance == Decimal("5000") + assert sim.positions["BTCUSDT"] == Decimal("0.1") + + +def test_simulator_sell_increases_balance(): + sim = OrderSimulator(Decimal("10000")) + buy_signal = make_signal("BTCUSDT", OrderSide.BUY, "50000", "0.1") + sim.execute(buy_signal) + balance_after_buy = sim.balance + + sell_signal = make_signal("BTCUSDT", OrderSide.SELL, "55000", "0.1") + result = sim.execute(sell_signal) + assert result is True + assert sim.balance > balance_after_buy + # Profit: sold at 55000, bought at 50000 → gain 500 + assert sim.balance == Decimal("10000") - Decimal("5000") + Decimal("5500") + + +def test_simulator_reject_buy_insufficient_balance(): + sim = OrderSimulator(Decimal("100")) + signal = make_signal("BTCUSDT", OrderSide.BUY, "50000", "0.1") + result = sim.execute(signal) + assert result is False + assert sim.balance == Decimal("100") + assert sim.positions.get("BTCUSDT", Decimal("0")) == Decimal("0") + + +def test_simulator_trade_history(): + sim = OrderSimulator(Decimal("10000")) + signal = make_signal("BTCUSDT", OrderSide.BUY, "50000", "0.1") + sim.execute(signal) + assert len(sim.trades) == 1 + trade = sim.trades[0] + assert trade.symbol == "BTCUSDT" + assert trade.side == OrderSide.BUY + assert trade.price == Decimal("50000") + assert trade.quantity == Decimal("0.1") -- cgit v1.2.3