Files
crypto_trading/backend/tests/test_engine.py
T
matitos 9c3bee87ac
CI/CD / backend (push) Successful in 5m18s
CI/CD / frontend (push) Successful in 5m2s
CI/CD / build (push) Successful in 34s
CI/CD / cleanup (push) Successful in 6s
CI/CD / deploy (push) Successful in 22s
Crypto trading bot: engine, brokers, backtester, dashboard, deploy
FastAPI backend with a single-strategy async engine, paper and Binance
brokers, pure-python indicators, a backtester that shares the engine's
portfolio math, and a Vue dashboard with a live WebSocket feed. Adds the
Gitea Actions CI/CD workflow that deploys to Traefik via docker_default.
2026-09-18 17:02:33 +00:00

111 lines
3.5 KiB
Python

from __future__ import annotations
from typing import ClassVar
import pytest
from app.broker.paper import PaperBroker
from app.config import Settings
from app.engine.engine import TradingEngine
from app.engine.strategies.base import Strategy, StrategyContext
from app.market.fake import FakeMarketData
from app.models import OrderSide, PositionSide
from app.schemas import Candle
from app.store import Store
class ToggleStrategy(Strategy):
key: ClassVar[str] = "test_toggle"
name: ClassVar[str] = "Toggle"
default_params: ClassVar[dict] = {}
def on_candle(self, ctx: StrategyContext) -> None:
if ctx.is_flat:
ctx.enter_long(size=0.5)
else:
ctx.close()
def build_engine(settings: Settings, store: Store, market: FakeMarketData) -> TradingEngine:
return TradingEngine(
feed=market,
broker=PaperBroker(fee_bps=0, slippage_bps=0),
store=store,
settings=settings,
)
async def test_manual_open_and_close_pnl(settings: Settings, store: Store) -> None:
market = FakeMarketData.from_closes([100.0])
engine = build_engine(settings, store, market)
engine.symbol = "BTC/USDT"
engine.last_price = 100.0
await engine.manual_order(OrderSide.BUY, qty=1.0, quote_amount=None, reason="test")
assert engine.position is not None
assert engine.cash == pytest.approx(9_900.0)
assert engine.equity == pytest.approx(10_000.0)
engine.last_price = 110.0
assert engine.equity == pytest.approx(10_010.0)
assert engine.unrealized_pnl == pytest.approx(10.0)
await engine.close_current_position()
assert engine.position is None
assert engine.realized_pnl == pytest.approx(10.0)
assert engine.cash == pytest.approx(10_010.0)
async def test_stop_loss_exits_at_level(settings: Settings, store: Store) -> None:
market = FakeMarketData.from_closes([100.0])
engine = build_engine(settings, store, market)
engine.symbol = "BTC/USDT"
engine.last_price = 100.0
await engine._open_position(
PositionSide.LONG,
size=0.0,
stop_loss=95.0,
take_profit=120.0,
reason="entry",
qty_override=1.0,
)
await engine._check_risk(Candle(ts=0, open=100, high=101, low=94, close=96, volume=1))
assert engine.position is None
assert engine.realized_pnl == pytest.approx(-5.0)
async def test_take_profit_exits_at_level(settings: Settings, store: Store) -> None:
market = FakeMarketData.from_closes([100.0])
engine = build_engine(settings, store, market)
engine.symbol = "BTC/USDT"
engine.last_price = 100.0
await engine._open_position(
PositionSide.LONG,
size=0.0,
stop_loss=95.0,
take_profit=110.0,
reason="entry",
qty_override=1.0,
)
await engine._check_risk(Candle(ts=0, open=100, high=111, low=100, close=109, volume=1))
assert engine.position is None
assert engine.realized_pnl == pytest.approx(10.0)
async def test_tick_runs_strategy_and_opens_position(settings: Settings, store: Store) -> None:
market = FakeMarketData.from_closes([100.0, 100.0])
engine = build_engine(settings, store, market)
engine.strategy = ToggleStrategy()
engine.strategy_key = "test_toggle"
engine.symbol = "BTC/USDT"
await engine.tick()
assert engine.position is not None
assert engine.position.side == PositionSide.LONG
market.append(Candle(ts=99, open=110, high=110, low=110, close=110, volume=1))
await engine.tick()
assert engine.position is None