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

+68
View File
@@ -0,0 +1,68 @@
from __future__ import annotations
import asyncio
from app.config import Settings
from app.notify import FanoutNotifier, Notifier, WebhookNotifier, build_notifier
from app.runtime import Runtime
class RecordingNotifier(Notifier):
def __init__(self, name: str = "rec", fail: bool = False) -> None:
self.name = name
self.fail = fail
self.messages: list[tuple[str, str]] = []
async def send(self, level: str, message: str) -> None:
if self.fail:
raise RuntimeError("boom")
self.messages.append((level, message))
async def test_fanout_sends_to_all() -> None:
first = RecordingNotifier("a")
second = RecordingNotifier("b")
await FanoutNotifier([first, second]).send("trade", "hello")
assert first.messages == [("trade", "hello")]
assert second.messages == [("trade", "hello")]
async def test_fanout_survives_a_failing_channel() -> None:
bad = RecordingNotifier("bad", fail=True)
good = RecordingNotifier("good")
await FanoutNotifier([bad, good]).send("error", "still delivered")
assert good.messages == [("error", "still delivered")]
def test_build_notifier_none_without_config() -> None:
assert build_notifier(Settings(telegram_bot_token="", notify_webhook_url="")) is None
def test_build_notifier_telegram_requires_both_fields() -> None:
assert build_notifier(Settings(telegram_bot_token="t", telegram_chat_id="")) is None
notifier = build_notifier(Settings(telegram_bot_token="t", telegram_chat_id="c"))
assert notifier is not None
def test_build_notifier_webhook() -> None:
notifier = build_notifier(Settings(notify_webhook_url="https://example.com/hook"))
assert isinstance(notifier, FanoutNotifier)
assert any(isinstance(n, WebhookNotifier) for n in notifier.notifiers)
async def test_runtime_notifies_on_trade_events() -> None:
recorder = RecordingNotifier()
settings = Settings(telegram_bot_token="", notify_webhook_url="")
runtime = Runtime(settings, notifier=recorder)
runtime._broadcast({"type": "event", "data": {"level": "trade", "message": "opened long"}})
await asyncio.gather(*list(runtime._notify_tasks))
assert recorder.messages == [("trade", "opened long")]
async def test_runtime_ignores_info_events() -> None:
recorder = RecordingNotifier()
settings = Settings(telegram_bot_token="", notify_webhook_url="")
runtime = Runtime(settings, notifier=recorder)
runtime._broadcast({"type": "event", "data": {"level": "info", "message": "started"}})
await asyncio.gather(*list(runtime._notify_tasks))
assert recorder.messages == []