Crypto trading bot: engine, brokers, backtester, dashboard, deploy
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

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:
matitos committed 2026-09-18 17:02:33 +00:00
1 parent 98028248c2
commit 9c3bee87ac
73 files changed
+9759 -1

No files matched your search

+140
View File
@@ -0,0 +1,140 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable
from contextlib import suppress
from typing import Any
from app.backtest import Backtester
from app.broker import build_broker
from app.broker.base import Broker
from app.config import Settings
from app.engine.engine import TradingEngine
from app.engine.strategies import get_strategy
from app.market import build_market_data
from app.market.base import MarketData
from app.schemas import BacktestRequest, BacktestResult, StatusOut
from app.store import Store
FeedFactory = Callable[[Settings], MarketData]
BrokerFactory = Callable[[Settings], Broker]
class Runtime:
"""Owns the active engine and fans out live updates to WebSocket clients."""
def __init__(
self,
settings: Settings,
store: Store | None = None,
*,
feed_factory: FeedFactory = build_market_data,
broker_factory: BrokerFactory = build_broker,
) -> None:
self.settings = settings
self.store = store or Store()
self._feed_factory = feed_factory
self._broker_factory = broker_factory
self.engine: TradingEngine | None = None
self._subscribers: set[asyncio.Queue] = set()
# --- pub/sub ------------------------------------------------------
def subscribe(self) -> asyncio.Queue:
queue: asyncio.Queue = asyncio.Queue(maxsize=100)
self._subscribers.add(queue)
return queue
def unsubscribe(self, queue: asyncio.Queue) -> None:
self._subscribers.discard(queue)
def _broadcast(self, payload: dict[str, Any]) -> None:
for queue in list(self._subscribers):
if queue.full():
with suppress(asyncio.QueueEmpty):
queue.get_nowait()
with suppress(asyncio.QueueFull):
queue.put_nowait(payload)
# --- lifecycle ----------------------------------------------------
async def start(
self,
strategy_key: str,
params: dict[str, Any] | None = None,
*,
symbol: str | None = None,
timeframe: str | None = None,
starting_balance: float | None = None,
) -> StatusOut:
if self.engine is not None and self.engine.running:
await self.stop()
engine = TradingEngine(
feed=self._feed_factory(self.settings),
broker=self._broker_factory(self.settings),
store=self.store,
settings=self.settings,
)
engine.set_publisher(self._broadcast)
self.engine = engine
await engine.start(
strategy_key,
params,
symbol=symbol,
timeframe=timeframe,
starting_balance=starting_balance,
)
return engine.status()
async def stop(self) -> StatusOut:
if self.engine is not None:
await self.engine.stop()
return self.engine.status()
return self._idle_status()
async def run_backtest(self, request: BacktestRequest) -> BacktestResult:
symbol = request.symbol or self.settings.symbol
timeframe = request.timeframe or self.settings.timeframe
strategy_cls = get_strategy(request.strategy_key)
strategy = strategy_cls(request.params)
feed = self._feed_factory(self.settings)
try:
candles = await feed.fetch_candles(symbol, timeframe, request.limit)
finally:
await feed.close()
backtester = Backtester(
fee_bps=self.settings.fee_bps,
slippage_bps=self.settings.slippage_bps,
starting_balance=request.starting_balance or self.settings.starting_balance,
)
return backtester.run(candles, strategy, symbol, timeframe)
async def shutdown(self) -> None:
if self.engine is not None:
await self.engine.shutdown()
def status(self) -> StatusOut:
if self.engine is not None:
return self.engine.status()
return self._idle_status()
def _idle_status(self) -> StatusOut:
return StatusOut(
running=False,
mode=self.settings.mode,
exchange=self.settings.exchange,
symbol=self.settings.symbol,
timeframe=self.settings.timeframe,
strategy_key=None,
strategy_name=None,
params={},
price=None,
cash=self.settings.starting_balance,
equity=self.settings.starting_balance,
starting_balance=self.settings.starting_balance,
realized_pnl=0.0,
unrealized_pnl=0.0,
total_pnl=0.0,
open_positions=0,
last_tick=None,
)