Phase 0: project skeleton

- pyproject.toml with uv, deps for runtime + gui + backtest + dev
- ruff/mypy strict config, pre-commit hooks for ruff/mypy/pytest
- src/cerbero_bite/ layout with empty modules ready for Phase 1+
- structlog JSONL logger with daily rotation
- click CLI with placeholder subcommands (status, start, kill-switch,
  gui, replay, config hash, audit verify)
- 6 smoke tests passing, mypy --strict clean, ruff clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 23:10:30 +02:00
commit 881bc8a1bf
40 changed files with 6018 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
"""Command-line interface for Cerbero Bite.
The CLI is the single user-facing entry point. Each subcommand maps to a
specific operational action documented in ``docs/06-operational-flow.md``
and ``docs/07-risk-controls.md``. All commands are placeholders in
Phase 0; subsequent phases will replace the bodies with real logic
without changing the surface.
"""
from __future__ import annotations
import sys
from pathlib import Path
import click
from rich.console import Console
from cerbero_bite import __version__
from cerbero_bite.logging import configure as configure_logging
from cerbero_bite.logging import get_logger
console = Console()
log = get_logger("cli")
def _phase0_notice(action: str) -> None:
console.print(f"[yellow]\\[phase 0 placeholder][/yellow] {action}")
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
@click.version_option(__version__, prog_name="cerbero-bite")
@click.option(
"--log-dir",
type=click.Path(file_okay=False, path_type=Path),
default=Path("data/log"),
show_default=True,
help="Directory for JSONL log files.",
)
@click.option(
"--log-level",
type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR"], case_sensitive=False),
default="INFO",
show_default=True,
)
@click.pass_context
def main(ctx: click.Context, log_dir: Path, log_level: str) -> None:
"""Cerbero Bite — rule-based ETH credit spread engine."""
configure_logging(log_dir=log_dir, level=log_level.upper())
ctx.ensure_object(dict)
ctx.obj["log_dir"] = log_dir
@main.command()
def status() -> None:
"""Print engine status snapshot."""
console.print(
f"[bold cyan]Cerbero Bite[/bold cyan] v{__version__}\n"
f"engine state: [yellow]idle[/yellow]\n"
f"kill_switch: [green]0 (disarmed)[/green]\n"
f"open positions: 0\n"
f"phase: 0 (skeleton)"
)
@main.command()
def start() -> None:
"""Start the engine main loop (scheduler + monitoring)."""
_phase0_notice("start command not yet implemented; engine remains idle.")
@main.command()
def stop() -> None:
"""Gracefully stop a running engine."""
_phase0_notice("stop command not yet implemented.")
@main.command(name="dry-run")
def dry_run() -> None:
"""Run the decision loop once in dry-run mode (no MCP writes)."""
_phase0_notice("dry-run command not yet implemented.")
@main.group(name="kill-switch")
def kill_switch() -> None:
"""Manage the engine kill switch."""
@kill_switch.command(name="arm")
@click.option("--reason", required=True, help="Why you are arming the kill switch.")
def kill_switch_arm(reason: str) -> None:
"""Arm the kill switch (engine refuses new entries)."""
_phase0_notice(f"kill-switch arm placeholder (reason: {reason!r}).")
@kill_switch.command(name="disarm")
@click.option("--reason", required=True, help="Why you are disarming.")
def kill_switch_disarm(reason: str) -> None:
"""Disarm the kill switch."""
_phase0_notice(f"kill-switch disarm placeholder (reason: {reason!r}).")
@main.command()
def gui() -> None:
"""Launch the Streamlit dashboard."""
_phase0_notice("gui command not yet implemented (will run streamlit on 127.0.0.1:8765).")
@main.command()
@click.option("--from", "date_from", required=True, help="ISO date YYYY-MM-DD.")
@click.option("--to", "date_to", required=True, help="ISO date YYYY-MM-DD.")
@click.option("--capital", type=float, default=1500.0, show_default=True)
@click.option("--dry-run/--no-dry-run", default=True, show_default=True)
def replay(date_from: str, date_to: str, capital: float, dry_run: bool) -> None:
"""Replay historical period through the decision engine."""
_phase0_notice(
f"replay {date_from}{date_to}, capital={capital}, dry_run={dry_run}"
)
@main.group()
def config() -> None:
"""Strategy configuration utilities."""
@config.command(name="hash")
def config_hash() -> None:
"""Compute and print SHA-256 of strategy.yaml."""
_phase0_notice("config hash placeholder; will read strategy.yaml and compute SHA-256.")
@main.group()
def audit() -> None:
"""Audit log utilities."""
@audit.command(name="verify")
def audit_verify() -> None:
"""Verify audit chain integrity."""
_phase0_notice("audit verify placeholder; will walk audit.log hash chain.")
def _entrypoint() -> None:
"""Wrapper used by ``cerbero-bite`` console script."""
try:
main(prog_name="cerbero-bite")
except KeyboardInterrupt:
console.print("[red]interrupted[/red]")
sys.exit(130)
if __name__ == "__main__":
_entrypoint()