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.
This commit is contained in:
1 parent
98028248c2
commit
9c3bee87ac
73 files changed
+9759
-1
No files matched your search
@@ -0,0 +1,110 @@
|
||||
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
|
||||
Reference in new issue
Block a user