Risk controls, alerts, API token, more exchanges, presets
- 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:
1 parent
9c3bee87ac
commit
03dfa369e5
35 files changed
+1409
-45
No files matched your search
@@ -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 == []
|
||||
Reference in new issue
Block a user