Risk controls, alerts, API token, more exchanges, presets
CI/CD / backend (pull_request) Successful in 6m18s
CI/CD / frontend (pull_request) Successful in 4m55s
CI/CD / build (pull_request) Skipped
CI/CD / deploy (pull_request) Skipped
CI/CD / cleanup (pull_request) Skipped

- RiskManager halts + flattens on max daily loss / drawdown, caps position
  size and trades per day; adds a manual kill switch (POST /api/bot/kill).
- Telegram and webhook notifications on trades, errors and halts.
- Optional API bearer token on /api, injected server-side by the frontend
  nginx so the browser never sees it.
- Kraken and Coinbase market data (public); Coinbase live broker. Live on an
  unsupported exchange now errors instead of silently falling back to paper.
- Strategy presets backed by the StrategyConfig table (CRUD API + UI).
- Idempotent additive-column migration so existing SQLite databases upgrade in
  place (fixes the new strategy_configs.name column).
This commit is contained in:
matitos committed 2026-09-18 19:26:29 +00:00
1 parent 9c3bee87ac
commit 03dfa369e5
35 files changed
+1409 -45

No files matched your search

+32
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import asyncio
import logging
from collections.abc import Callable
from contextlib import suppress
from typing import Any
@@ -13,11 +14,15 @@ 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.notify import Notifier, build_notifier
from app.schemas import BacktestRequest, BacktestResult, StatusOut
from app.store import Store
logger = logging.getLogger(__name__)
FeedFactory = Callable[[Settings], MarketData]
BrokerFactory = Callable[[Settings], Broker]
NotifierFactory = Callable[[Settings], Notifier | None]
class Runtime:
@@ -30,13 +35,19 @@ class Runtime:
*,
feed_factory: FeedFactory = build_market_data,
broker_factory: BrokerFactory = build_broker,
notifier: Notifier | None = None,
notifier_factory: NotifierFactory = build_notifier,
notify_levels: set[str] | None = None,
) -> None:
self.settings = settings
self.store = store or Store()
self._feed_factory = feed_factory
self._broker_factory = broker_factory
self.notifier = notifier if notifier is not None else notifier_factory(settings)
self.notify_levels = notify_levels or {"trade", "error"}
self.engine: TradingEngine | None = None
self._subscribers: set[asyncio.Queue] = set()
self._notify_tasks: set[asyncio.Task] = set()
# --- pub/sub ------------------------------------------------------
def subscribe(self) -> asyncio.Queue:
@@ -54,6 +65,19 @@ class Runtime:
queue.get_nowait()
with suppress(asyncio.QueueFull):
queue.put_nowait(payload)
if payload.get("type") == "event":
self._maybe_notify(payload["data"])
def _maybe_notify(self, event: dict[str, Any]) -> None:
if self.notifier is None or event.get("level") not in self.notify_levels:
return
message = event.get("message", "")
try:
task = asyncio.create_task(self.notifier.send(event["level"], message))
except RuntimeError: # no running loop (e.g. sync test context)
return
self._notify_tasks.add(task)
task.add_done_callback(self._notify_tasks.discard)
# --- lifecycle ----------------------------------------------------
async def start(
@@ -90,6 +114,12 @@ class Runtime:
return self.engine.status()
return self._idle_status()
async def kill(self, reason: str = "manual kill switch") -> StatusOut:
if self.engine is not None:
await self.engine.kill(reason)
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
@@ -112,6 +142,8 @@ class Runtime:
async def shutdown(self) -> None:
if self.engine is not None:
await self.engine.shutdown()
if self.notifier is not None:
await self.notifier.close()
def status(self) -> StatusOut:
if self.engine is not None: