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

+64
View File
@@ -7,6 +7,7 @@ import pytest
from app.broker.paper import PaperBroker
from app.config import Settings
from app.engine.engine import TradingEngine
from app.engine.risk import RiskManager # noqa: F401 (documents the dependency)
from app.engine.strategies.base import Strategy, StrategyContext
from app.market.fake import FakeMarketData
from app.models import OrderSide, PositionSide
@@ -94,6 +95,69 @@ async def test_take_profit_exits_at_level(settings: Settings, store: Store) -> N
assert engine.realized_pnl == pytest.approx(10.0)
async def test_daily_loss_halts_and_flattens(settings: Settings, store: Store) -> None:
settings.max_daily_loss = 50.0
market = FakeMarketData.from_closes([100.0])
engine = build_engine(settings, store, market)
engine.symbol = "BTC/USDT"
engine.last_price = 100.0
engine.risk.reset(10_000)
await engine.manual_order(OrderSide.BUY, qty=1.0, quote_amount=None, reason="test")
market.candles[0] = Candle(ts=0, open=100, high=100, low=40, close=40, volume=1)
await engine.tick()
assert engine.risk.halted is True
assert engine.position is None
assert engine.running is False
async def test_kill_switch_flattens_and_stops(settings: Settings, store: Store) -> None:
market = FakeMarketData.from_closes([100.0])
engine = build_engine(settings, store, market)
engine.symbol = "BTC/USDT"
engine.last_price = 100.0
await engine.manual_order(OrderSide.BUY, qty=1.0, quote_amount=None, reason="test")
assert engine.position is not None
await engine.kill("panic")
assert engine.running is False
assert engine.risk.halted is True
assert engine.position is None
assert engine.status().halt_reason == "panic"
async def test_max_trades_per_day_blocks_new_entries(settings: Settings, store: Store) -> None:
settings.max_trades_per_day = 1
market = FakeMarketData.from_closes([100.0])
engine = build_engine(settings, store, market)
engine.symbol = "BTC/USDT"
engine.last_price = 100.0
engine.risk.reset(10_000)
await engine.manual_order(OrderSide.BUY, qty=1.0, quote_amount=None, reason="first")
assert engine.position is not None
await engine.close_current_position()
await engine.manual_order(OrderSide.BUY, qty=1.0, quote_amount=None, reason="second")
assert engine.position is None
async def test_max_position_pct_clamps_size(settings: Settings, store: Store) -> None:
settings.max_position_pct = 0.5
market = FakeMarketData.from_closes([100.0])
engine = build_engine(settings, store, market)
engine.symbol = "BTC/USDT"
engine.last_price = 100.0
engine.risk.reset(10_000)
await engine._open_position(
PositionSide.LONG, size=1.0, stop_loss=None, take_profit=None, reason="big"
)
assert engine.position is not None
assert engine.position.qty == pytest.approx(50.0)
async def test_tick_runs_strategy_and_opens_position(settings: Settings, store: Store) -> None:
market = FakeMarketData.from_closes([100.0, 100.0])
engine = build_engine(settings, store, market)