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