spb/crypto-trade-bot
Public
Python 54.7%
TypeScript 42.5%
SQL 1.9%
1"""Integration tests against a local Postgres (ktrade_test) and Redis db 15. Kraken calls are mocked."""23import os45import httpx6import pytest7import respx89from ktrade.api.auth import hash_password10from ktrade.db.pool import dsn, migrate1112pytestmark = pytest.mark.skipif(os.environ.get("KTRADE_SKIP_DB") == "1", reason="db tests disabled")131415@pytest.fixture16async def client(monkeypatch):17 import asyncpg1819 from ktrade.config import get_settings2021 get_settings.cache_clear()22 monkeypatch.setenv("KTRADE_OWNER_PASSWORD_HASH", hash_password("hunter2"))23 monkeypatch.setenv("KTRADE_APP_BASE_URL", "http://testserver")24 url = os.environ["KTRADE_DATABASE_URL"]25 conn = await asyncpg.connect(dsn(url))26 await conn.execute("DROP SCHEMA public CASCADE; CREATE SCHEMA public")27 await conn.close()28 await migrate(url)29 from ktrade.api.main import create_app3031 app = create_app()32 async with app.router.lifespan_context(app):33 async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://testserver") as c:34 yield c, app35 get_settings.cache_clear()363738async def test_health_and_auth_flow(client):39 c, app = client40 r = await c.get("/api/health")41 assert r.status_code == 200 and r.json()["db"] == "ok"42 assert (await c.get("/api/pairs")).status_code == 40143 r = await c.post("/api/auth/login", json={"password": "wrong"}, headers={"origin": "http://testserver"})44 assert r.status_code == 40145 r = await c.post("/api/auth/login", json={"password": "hunter2"}, headers={"origin": "http://testserver"})46 assert r.status_code == 200 and "ktrade_session" in r.cookies47 assert (await c.get("/api/auth/me")).json()["authenticated"] is True48 # CSRF: mutation without same-origin header is rejected49 r = await c.post("/api/risk/kill-switch", json={"active": True})50 assert r.status_code == 40351 r = await c.post("/api/risk/kill-switch", json={"active": True, "reason": "test"}, headers={"origin": "http://testserver"})52 assert r.status_code == 200 and r.json()["active"] is True53 audit = (await c.get("/api/audit")).json()54 assert audit["chain_valid"] and any(a["action"] == "risk.kill_switch" for a in audit["items"])555657async def test_backtest_end_to_end(client):58 import asyncio5960 from tests.conftest import synthetic_candles6162 c, app = client63 st = app.state.ktrade64 await c.post("/api/auth/login", json={"password": "hunter2"}, headers={"origin": "http://testserver"})65 df = synthetic_candles(n=1200, seed=21)66 await st.pool.execute("INSERT INTO pairs (pair_id, altname, wsname, base, quote, status, in_universe, lot_decimals, ordermin, costmin) VALUES ('XXBTZUSD','XBTUSD','BTC/USD','XXBT','ZUSD','online',true,8,0.0001,0.5)")67 await st.pool.executemany("INSERT INTO candles (symbol, interval_min, ts, open, high, low, close, vwap, volume, count, closed) VALUES ('BTC/USD',60,$1,$2,$3,$4,$5,$6,$7,$8,true)",68 [(int(r.ts), r.open, r.high, r.low, r.close, r.vwap, r.volume, int(r.count)) for r in df.itertuples()])69 r = await c.get("/api/market/candles", params={"symbol": "BTC/USD", "interval": 60, "limit": 10})70 assert len(r.json()["items"]) == 1071 r = await c.post("/api/backtests", json={"strategy": "tsmom", "symbol": "BTC/USD", "interval_min": 60, "holdout_frac": 0.2, "sensitivity": {"x": "lookback", "x_values": [24, 48], "y": "ema_slow", "y_values": [60, 80]}}, headers={"origin": "http://testserver"})72 assert r.status_code == 20273 rid = r.json()["id"]74 for _ in range(100):75 await asyncio.sleep(0.2)76 run = (await c.get(f"/api/backtests/{rid}")).json()77 if run["status"] in ("done", "failed"):78 break79 assert run["status"] == "done", run.get("error")80 assert run["metrics"]["bars"] > 0 and "sharpe" in run["metrics"]81 assert run["provenance"]["dataset_hash"] and run["artifacts"]["benchmarks"]["buy_and_hold"]82 assert run["artifacts"]["holdout"]["metrics"]["bars"] > 083 assert run["artifacts"]["sensitivity"]["z"] and run["artifacts"]["stress"]["fees_x2"]84 r = await c.get(f"/api/backtests/{rid}/trades.csv")85 assert r.status_code == 20086 r = await c.post("/api/backtests", json={"strategy": "tsmom", "symbol": "BTC/USD", "params": {"nope": 1}}, headers={"origin": "http://testserver"})87 assert r.status_code == 422888990@respx.mock91async def test_rest_client_parsing():92 from ktrade.kraken.rest import KrakenREST9394 respx.get("https://api.kraken.com/0/public/OHLC").mock(return_value=httpx.Response(200, json={"error": [], "result": {"XXBTZUSD": [[1700000000, "1", "2", "0.5", "1.5", "1.2", "10", 3]], "last": 1700000000}}))95 respx.get("https://api.kraken.com/0/public/AssetPairs").mock(return_value=httpx.Response(200, json={"error": [], "result": {"XXBTZUSD": {"altname": "XBTUSD", "wsname": "XBT/USD", "base": "XXBT", "quote": "ZUSD", "ordermin": "0.0001", "costmin": "0.5", "tick_size": "0.1", "pair_decimals": 1, "lot_decimals": 8, "status": "online"}}}))96 respx.get("https://api.kraken.com/0/public/Time").mock(return_value=httpx.Response(200, json={"error": ["EAPI:Rate limit exceeded"]}))97 rest = KrakenREST(public_min_interval=0)98 candles, last = await rest.ohlc("XXBTZUSD", 1)99 assert candles[0].close == 1.5 and last == 1700000000 and candles[0].closed_at(1) == 1700000060100 pairs = await rest.asset_pairs()101 assert pairs["XXBTZUSD"].wsname == "BTC/USD" and float(pairs["XXBTZUSD"].ordermin) == 0.0001102 with pytest.raises(Exception, match="Rate limit"):103 await rest.time()104 await rest.aclose()105