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,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,
|
||||
)
|
||||
Reference in new issue
Block a user