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
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user