SPB Git forge
1commits 1branches 0releases
189.0 KBsize
maindefault branch
4 h agolast push
Python 54.7% TypeScript 42.5% SQL 1.9%
5.5 KB · 105 lines python
Raw Blame History
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