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

+67
View File
@@ -76,6 +76,73 @@ def test_backtest_limit_validation(runtime: Runtime) -> None:
)
def test_kill_endpoint(runtime: Runtime) -> None:
app = create_app(runtime.settings, runtime)
with TestClient(app) as client:
client.post("/api/bot/start", json={"strategy_key": "sma_cross"})
killed = client.post("/api/bot/kill", json={"reason": "test kill"}).json()
assert killed["running"] is False
assert killed["halted"] is True
assert killed["halt_reason"] == "test kill"
def test_status_exposes_risk_fields(runtime: Runtime) -> None:
app = create_app(runtime.settings, runtime)
with TestClient(app) as client:
status = client.get("/api/status").json()
assert {"halted", "halt_reason", "trades_today", "day_pnl", "drawdown_pct"} <= set(status)
def test_preset_crud(runtime: Runtime) -> None:
app = create_app(runtime.settings, runtime)
with TestClient(app) as client:
created = client.post(
"/api/presets",
json={
"name": "My SMA",
"strategy_key": "sma_cross",
"symbol": "BTC/USDT",
"timeframe": "1h",
"params": {"fast": 5, "slow": 20},
},
)
assert created.status_code == 200
preset = created.json()
assert preset["name"] == "My SMA"
listed = client.get("/api/presets").json()
assert any(p["id"] == preset["id"] for p in listed)
assert client.delete(f"/api/presets/{preset['id']}").status_code == 200
assert client.get("/api/presets").json() == []
def test_preset_unknown_strategy_404(runtime: Runtime) -> None:
app = create_app(runtime.settings, runtime)
with TestClient(app) as client:
response = client.post(
"/api/presets",
json={
"strategy_key": "nope",
"symbol": "BTC/USDT",
"timeframe": "1m",
"params": {},
},
)
assert response.status_code == 404
def test_api_token_enforced(runtime: Runtime) -> None:
runtime.settings.api_token = "secret"
app = create_app(runtime.settings, runtime)
with TestClient(app) as client:
assert client.get("/api/status").status_code == 401
ok = client.get("/api/status", headers={"X-API-Token": "secret"})
assert ok.status_code == 200
# Health stays open.
assert client.get("/api/health").status_code == 200
def test_websocket_sends_initial_status(runtime: Runtime) -> None:
app = create_app(runtime.settings, runtime)
with TestClient(app) as client, client.websocket_connect("/api/ws") as websocket: