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:
@@ -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()
|
||||
Reference in New Issue
Block a user