"""ktrade command line.""" from __future__ import annotations import asyncio import getpass import json import uuid import typer from ktrade.config import get_settings from ktrade.logging import configure_logging app = typer.Typer(help="Kraken quant trading platform — operator CLI", no_args_is_help=True) @app.command() def migrate() -> None: """Apply SQL migrations.""" from ktrade.db.pool import migrate as _m s = get_settings() configure_logging(s.log_level, False) applied = asyncio.run(_m(s.database_url)) typer.echo(f"applied: {applied or 'nothing (up to date)'}") @app.command() def api(host: str | None = None, port: int | None = None, reload: bool = False) -> None: """Run the FastAPI server.""" import uvicorn s = get_settings() 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) @app.command() def worker() -> None: """Run the market-data ingestion worker (pairs, OHLC accumulation, WebSocket stream).""" from ktrade.workers.ingest import run_worker s = get_settings() configure_logging(s.log_level, s.log_json) asyncio.run(run_worker(s)) @app.command() def backfill() -> None: """One-shot pair refresh + OHLC poll for the universe.""" from ktrade.db.pool import create_pool from ktrade.ingestion.backfill import backfill_all from ktrade.ingestion.pairs import refresh_pairs from ktrade.kraken.rest import KrakenREST s = get_settings() configure_logging(s.log_level, False) async def _go(): pool = await create_pool(s.database_url) rest = KrakenREST(s.kraken_rest_url, tier=s.kraken_tier) pairs = await refresh_pairs(rest, pool, s.universe) res = await backfill_all(rest, pool, pairs, s.candle_intervals) await rest.aclose() await pool.close() return res typer.echo(json.dumps(asyncio.run(_go()), indent=1)) @app.command("set-owner-password") def set_owner_password(password: str | None = typer.Option(None, help="omit to be prompted")) -> None: """Print the bcrypt hash to put in KTRADE_OWNER_PASSWORD_HASH (never stores the password).""" from ktrade.api.auth import hash_password pw = password or getpass.getpass("owner password: ") typer.echo(hash_password(pw)) @app.command() def preflight() -> None: """Probe configured Kraken keys (read endpoints + validate-only order). Never places orders.""" import redis.asyncio as aioredis from ktrade.api.deps import AppState from ktrade.db.pool import create_pool from ktrade.execution.preflight import run_preflight from ktrade.kraken.rest import KrakenREST s = get_settings() configure_logging(s.log_level, False) async def _go(): pool = await create_pool(s.database_url) 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)) try: return await run_preflight(st) finally: await st.rest.aclose() await pool.close() typer.echo(json.dumps(asyncio.run(_go()), indent=1, default=str)) @app.command() def backtest(strategy: str, symbol: str = "BTC/USD", interval: int = 60, params: str = "{}", holdout: float = 0.2, cash: float = 10000.0) -> None: """Run a backtest from the CLI and print the metrics (stored in backtest_runs).""" from ktrade.backtest.runner import execute_backtest from ktrade.db.pool import create_pool s = get_settings() configure_logging(s.log_level, False) run_id = uuid.uuid4() spec = {"strategy": strategy, "symbol": symbol, "interval_min": interval, "params": json.loads(params), "holdout_frac": holdout, "initial_cash": cash, "stress": True} async def _go(): pool = await create_pool(s.database_url) 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"]) await execute_backtest(pool, run_id, spec) row = await pool.fetchrow("SELECT metrics, status, error FROM backtest_runs WHERE id=$1", run_id) await pool.close() return dict(row) out = asyncio.run(_go()) typer.echo(f"run {run_id}: {out['status']} {out.get('error') or ''}") typer.echo(json.dumps(out["metrics"], indent=1, default=str)) @app.command() def reproduce(run_id: str) -> None: """Re-run a stored backtest from its provenance (same window, params, costs, seed) and compare metrics.""" import math from ktrade.backtest.costs import CostModel from ktrade.backtest.engine import run_backtest from ktrade.backtest.metrics import compute_metrics from ktrade.backtest.runner import dataset_hash from ktrade.db.pool import create_pool from ktrade.market.service import candles_frame from ktrade.risk.gateway import RiskLimits from ktrade.strategy.registry import get_strategy s = get_settings() async def _go(): pool = await create_pool(s.database_url) try: r = await pool.fetchrow("SELECT strategy, symbol, interval_min, metrics, provenance FROM backtest_runs WHERE id=$1 AND status='done'", uuid.UUID(run_id)) if not r: raise typer.BadParameter("run not found or not done") pv = r["provenance"] df = await candles_frame(pool, r["symbol"], r["interval_min"], pv["window"][0], pv["window"][1]) pair = await pool.fetchrow("SELECT lot_decimals, ordermin, costmin FROM pairs WHERE wsname=$1", r["symbol"]) same_data = dataset_hash(df) == pv["dataset_hash"] 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"]), risk_limits=RiskLimits(**pv["risk_limits"]) if pv.get("risk_limits") else None, 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) m = compute_metrics(res.equity, res.trades, r["interval_min"], pv["initial_cash"], res.fees_paid, res.slippage_paid, res.turnover) return same_data, r["metrics"], m finally: await pool.close() same_data, old, new = asyncio.run(_go()) typer.echo(f"dataset hash identical: {same_data}") ok = True for k in ("total_return", "sharpe", "max_drawdown", "n_trades", "fees_paid"): a, b = old.get(k), new.get(k) 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)) ok &= bool(match) typer.echo(f" {k:14s} stored={a!r:<24} rerun={b!r:<24} {'OK' if match else 'MISMATCH'}") typer.echo("REPRODUCIBLE" if ok and same_data else "NOT REPRODUCIBLE (data changed or code drift)") raise typer.Exit(0 if ok and same_data else 1) @app.command("audit-verify") def audit_verify() -> None: from ktrade.db.audit import verify_chain from ktrade.db.pool import create_pool s = get_settings() async def _go(): pool = await create_pool(s.database_url) try: return await verify_chain(pool) finally: await pool.close() ok, n = asyncio.run(_go()) typer.echo(f"audit chain valid={ok} entries={n}") raise typer.Exit(0 if ok else 1) if __name__ == "__main__": app()