SPB Git forge
1commits 1branches 0releases
189.0 KBsize
maindefault branch
4 h agolast push
Python 54.7% TypeScript 42.5% SQL 1.9%
7.5 KB · 196 lines python
Raw Blame History
1"""ktrade command line."""23from __future__ import annotations45import asyncio6import getpass7import json8import uuid910import typer1112from ktrade.config import get_settings13from ktrade.logging import configure_logging1415app = typer.Typer(help="Kraken quant trading platform — operator CLI", no_args_is_help=True)161718@app.command()19def migrate() -> None:20    """Apply SQL migrations."""21    from ktrade.db.pool import migrate as _m2223    s = get_settings()24    configure_logging(s.log_level, False)25    applied = asyncio.run(_m(s.database_url))26    typer.echo(f"applied: {applied or 'nothing (up to date)'}")272829@app.command()30def api(host: str | None = None, port: int | None = None, reload: bool = False) -> None:31    """Run the FastAPI server."""32    import uvicorn3334    s = get_settings()35    uvicorn.run("ktrade.api.main:app", host=host or s.api_host, port=port or s.api_port, reload=reload, log_level=s.log_level.lower(), proxy_headers=True)363738@app.command()39def worker() -> None:40    """Run the market-data ingestion worker (pairs, OHLC accumulation, WebSocket stream)."""41    from ktrade.workers.ingest import run_worker4243    s = get_settings()44    configure_logging(s.log_level, s.log_json)45    asyncio.run(run_worker(s))464748@app.command()49def backfill() -> None:50    """One-shot pair refresh + OHLC poll for the universe."""51    from ktrade.db.pool import create_pool52    from ktrade.ingestion.backfill import backfill_all53    from ktrade.ingestion.pairs import refresh_pairs54    from ktrade.kraken.rest import KrakenREST5556    s = get_settings()57    configure_logging(s.log_level, False)5859    async def _go():60        pool = await create_pool(s.database_url)61        rest = KrakenREST(s.kraken_rest_url, tier=s.kraken_tier)62        pairs = await refresh_pairs(rest, pool, s.universe)63        res = await backfill_all(rest, pool, pairs, s.candle_intervals)64        await rest.aclose()65        await pool.close()66        return res6768    typer.echo(json.dumps(asyncio.run(_go()), indent=1))697071@app.command("set-owner-password")72def set_owner_password(password: str | None = typer.Option(None, help="omit to be prompted")) -> None:73    """Print the bcrypt hash to put in KTRADE_OWNER_PASSWORD_HASH (never stores the password)."""74    from ktrade.api.auth import hash_password7576    pw = password or getpass.getpass("owner password: ")77    typer.echo(hash_password(pw))787980@app.command()81def preflight() -> None:82    """Probe configured Kraken keys (read endpoints + validate-only order). Never places orders."""83    import redis.asyncio as aioredis8485    from ktrade.api.deps import AppState86    from ktrade.db.pool import create_pool87    from ktrade.execution.preflight import run_preflight88    from ktrade.kraken.rest import KrakenREST8990    s = get_settings()91    configure_logging(s.log_level, False)9293    async def _go():94        pool = await create_pool(s.database_url)95        st = AppState(settings=s, pool=pool, redis=aioredis.from_url(s.redis_url), rest=KrakenREST(s.kraken_rest_url, s.kraken_api_key, s.kraken_api_secret, s.kraken_tier))96        try:97            return await run_preflight(st)98        finally:99            await st.rest.aclose()100            await pool.close()101102    typer.echo(json.dumps(asyncio.run(_go()), indent=1, default=str))103104105@app.command()106def backtest(strategy: str, symbol: str = "BTC/USD", interval: int = 60, params: str = "{}", holdout: float = 0.2, cash: float = 10000.0) -> None:107    """Run a backtest from the CLI and print the metrics (stored in backtest_runs)."""108    from ktrade.backtest.runner import execute_backtest109    from ktrade.db.pool import create_pool110111    s = get_settings()112    configure_logging(s.log_level, False)113    run_id = uuid.uuid4()114    spec = {"strategy": strategy, "symbol": symbol, "interval_min": interval, "params": json.loads(params), "holdout_frac": holdout, "initial_cash": cash, "stress": True}115116    async def _go():117        pool = await create_pool(s.database_url)118        await pool.execute("INSERT INTO backtest_runs (id, strategy, symbol, interval_min, params, label) VALUES ($1,$2,$3,$4,$5,'cli')", run_id, strategy, symbol, interval, spec["params"])119        await execute_backtest(pool, run_id, spec)120        row = await pool.fetchrow("SELECT metrics, status, error FROM backtest_runs WHERE id=$1", run_id)121        await pool.close()122        return dict(row)123124    out = asyncio.run(_go())125    typer.echo(f"run {run_id}: {out['status']} {out.get('error') or ''}")126    typer.echo(json.dumps(out["metrics"], indent=1, default=str))127128129@app.command()130def reproduce(run_id: str) -> None:131    """Re-run a stored backtest from its provenance (same window, params, costs, seed) and compare metrics."""132    import math133134    from ktrade.backtest.costs import CostModel135    from ktrade.backtest.engine import run_backtest136    from ktrade.backtest.metrics import compute_metrics137    from ktrade.backtest.runner import dataset_hash138    from ktrade.db.pool import create_pool139    from ktrade.market.service import candles_frame140    from ktrade.risk.gateway import RiskLimits141    from ktrade.strategy.registry import get_strategy142143    s = get_settings()144145    async def _go():146        pool = await create_pool(s.database_url)147        try:148            r = await pool.fetchrow("SELECT strategy, symbol, interval_min, metrics, provenance FROM backtest_runs WHERE id=$1 AND status='done'", uuid.UUID(run_id))149            if not r:150                raise typer.BadParameter("run not found or not done")151            pv = r["provenance"]152            df = await candles_frame(pool, r["symbol"], r["interval_min"], pv["window"][0], pv["window"][1])153            pair = await pool.fetchrow("SELECT lot_decimals, ordermin, costmin FROM pairs WHERE wsname=$1", r["symbol"])154            same_data = dataset_hash(df) == pv["dataset_hash"]155            res = run_backtest(get_strategy(r["strategy"])(pv["params"]), df, interval_min=r["interval_min"], initial_cash=pv["initial_cash"], cost_model=CostModel(**pv["cost_model"]),156                               risk_limits=RiskLimits(**pv["risk_limits"]) if pv.get("risk_limits") else None,157                               lot_decimals=pair["lot_decimals"] if pair else 8, ordermin=float(pair["ordermin"]) if pair else 0.0, costmin=float(pair["costmin"]) if pair else 0.0)158            m = compute_metrics(res.equity, res.trades, r["interval_min"], pv["initial_cash"], res.fees_paid, res.slippage_paid, res.turnover)159            return same_data, r["metrics"], m160        finally:161            await pool.close()162163    same_data, old, new = asyncio.run(_go())164    typer.echo(f"dataset hash identical: {same_data}")165    ok = True166    for k in ("total_return", "sharpe", "max_drawdown", "n_trades", "fees_paid"):167        a, b = old.get(k), new.get(k)168        match = (a == b) if isinstance(a, int) else (a is not None and b is not None and math.isclose(a, b, rel_tol=1e-9, abs_tol=1e-9))169        ok &= bool(match)170        typer.echo(f"  {k:14s} stored={a!r:<24} rerun={b!r:<24} {'OK' if match else 'MISMATCH'}")171    typer.echo("REPRODUCIBLE" if ok and same_data else "NOT REPRODUCIBLE (data changed or code drift)")172    raise typer.Exit(0 if ok and same_data else 1)173174175@app.command("audit-verify")176def audit_verify() -> None:177    from ktrade.db.audit import verify_chain178    from ktrade.db.pool import create_pool179180    s = get_settings()181182    async def _go():183        pool = await create_pool(s.database_url)184        try:185            return await verify_chain(pool)186        finally:187            await pool.close()188189    ok, n = asyncio.run(_go())190    typer.echo(f"audit chain valid={ok} entries={n}")191    raise typer.Exit(0 if ok else 1)192193194if __name__ == "__main__":195    app()196