spb/crypto-trade-bot
Public
Python 54.7%
TypeScript 42.5%
SQL 1.9%
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