diff --git a/README.md b/README.md index b5699bb..1922dce 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,69 @@ # coinhunter-cli -CoinHunter CLI is the executable tooling layer for CoinHunter. +

+ The executable CLI layer for CoinHunter.
+ Runtime-safe trading operations, precheck orchestration, review tooling, and market probes. +

-• Code lives in this repository. -• User runtime data lives in ~/.coinhunter/ by default. -• Hermes skills can call this CLI instead of embedding large script collections. -• Runtime locations can be overridden with COINHUNTER_HOME, HERMES_HOME, COINHUNTER_ENV_FILE, and HERMES_BIN. +

+ Python + Status + Architecture +

-## Install +## Why this repo exists + +CoinHunter is evolving from a loose bundle of automation scripts into a proper installable command-line tool. + +This repository is the tooling layer: + +- Code and executable behavior live here. +- User runtime state lives in `~/.coinhunter/` by default. +- Hermes skills can call this CLI instead of embedding large script collections. +- Runtime paths can be overridden with `COINHUNTER_HOME`, `HERMES_HOME`, `COINHUNTER_ENV_FILE`, and `HERMES_BIN`. + +In short: + +- `coinhunter-cli` = tool +- CoinHunter skill = strategy / workflow / prompting layer +- `~/.coinhunter` = user data, logs, state, reviews + +## Current architecture + +```text +coinhunter-cli/ +├── src/coinhunter/ +│ ├── cli.py # top-level command router +│ ├── runtime.py # runtime paths + env loading +│ ├── doctor.py # diagnostics +│ ├── paths.py # runtime path inspection +│ ├── commands/ # thin CLI adapters +│ ├── services/ # orchestration / application services +│ └── *.py # compatibility modules + legacy logic under extraction +└── README.md +``` + +The repo is actively being refactored toward a cleaner split: + +- `commands/` → argument / CLI adapters +- `services/` → orchestration and application workflows +- `runtime/` → paths, env, files, locks, config +- future `domain/` → trading and precheck core logic + +## Implemented command/service splits + +The first extraction pass is already live: + +- `smart-executor` → `commands.smart_executor` + `services.smart_executor_service` +- `precheck` → `commands.precheck` + `services.precheck_service` +- `precheck` internals now also have dedicated service modules for: + - `services.precheck_state` + - `services.precheck_snapshot` + - `services.precheck_analysis` + +This keeps behavior stable while giving the codebase a cleaner landing zone for deeper refactors. + +## Installation Editable install: @@ -15,38 +71,152 @@ Editable install: pip install -e . ``` -## Core commands +Run directly after install: ```bash +coinhunter --help coinhunter --version -coinhunter doctor -coinhunter paths +``` + +## Quickstart + +Initialize user state: + +```bash coinhunter init +``` + +Inspect runtime wiring: + +```bash +coinhunter paths +coinhunter doctor +``` + +Validate exchange credentials: + +```bash coinhunter check-api -coinhunter smart-executor balances +``` + +Run precheck / gate plumbing: + +```bash coinhunter precheck +coinhunter precheck --mark-run-requested "external-gate queued cron run" +coinhunter precheck --ack "analysis finished" +``` + +Inspect balances or execute trading actions: + +```bash +coinhunter smart-executor balances +coinhunter smart-executor status +coinhunter smart-executor hold +coinhunter smart-executor buy ENJUSDT 50 +coinhunter smart-executor sell-all ENJUSDT +``` + +Generate review data: + +```bash coinhunter review-context 12 +coinhunter review-engine 12 +``` + +Probe external market data: + +```bash coinhunter market-probe bybit-ticker BTCUSDT +coinhunter market-probe bybit-klines BTCUSDT 60 20 ``` ## Runtime model Default layout: -• ~/.coinhunter/ stores config, positions, logs, reviews, and state. -• ~/.hermes/.env stores exchange credentials unless COINHUNTER_ENV_FILE overrides it. -• hermes is discovered from PATH first, then ~/.local/bin/hermes, unless HERMES_BIN overrides it. +```text +~/.coinhunter/ +├── accounts.json +├── config.json +├── executions.json +├── notes.json +├── positions.json +├── watchlist.json +├── logs/ +├── reviews/ +└── state/ +``` -## Next refactor direction +Credential loading: -This repository now has a dedicated runtime layer and CLI diagnostics. The next major cleanup is to split command adapters from trading services so the internal architecture becomes: +- Binance credentials are read from `~/.hermes/.env` by default. +- `COINHUNTER_ENV_FILE` can point to a different env file. +- `hermes` is resolved from `PATH` first, then `~/.local/bin/hermes`, unless `HERMES_BIN` overrides it. -• commands/ -• services/ -• runtime/ -• domain logic +## Useful commands -The first split is already in place for: +### Diagnostics -• smart-executor -> commands.smart_executor + services.smart_executor_service -• precheck -> commands.precheck + services.precheck_service +```bash +coinhunter doctor +coinhunter paths +coinhunter check-api +``` + +### Trading and execution + +```bash +coinhunter smart-executor balances +coinhunter smart-executor status +coinhunter smart-executor hold +coinhunter smart-executor rebalance FROMUSDT TOUSDT +``` + +### Precheck and orchestration + +```bash +coinhunter precheck +coinhunter external-gate +coinhunter rotate-external-gate-log +``` + +### Review and market research + +```bash +coinhunter review-context 12 +coinhunter review-engine 12 +coinhunter market-probe bybit-ticker BTCUSDT +``` + +## Development notes + +This project is intentionally moving in small, safe refactor steps: + +1. Separate runtime concerns from hardcoded paths. +2. Move command dispatch into thin adapters. +3. Introduce orchestration services. +4. Extract reusable domain logic from large compatibility modules. +5. Keep cron / Hermes integration stable during migration. + +That means some compatibility modules still exist, but the direction is deliberate. + +## Near-term roadmap + +- Extract more logic from `smart_executor.py` into dedicated execution / portfolio services. +- Continue shrinking `precheck.py` by moving snapshot and analysis internals into reusable modules. +- Add `domain/` models for positions, signals, and trigger analysis. +- Add tests for runtime paths, precheck service behavior, and CLI stability. +- Evolve toward a more polished installable CLI workflow. + +## Philosophy + +CoinHunter should become: + +- more professional +- more maintainable +- safer to operate +- easier for humans and agents to call +- less dependent on prompt-only correctness + +This repo is where that evolution happens. diff --git a/src/coinhunter/precheck.py b/src/coinhunter/precheck.py index 3f68a06..787ceee 100755 --- a/src/coinhunter/precheck.py +++ b/src/coinhunter/precheck.py @@ -904,23 +904,15 @@ def update_state_after_observation(state: dict, snapshot: dict, analysis: dict): def mark_run_requested(note: str = ""): - state = load_state() - state["run_requested_at"] = utc_iso() - state["run_request_note"] = note - save_state(state) - print(json.dumps({"ok": True, "run_requested_at": state["run_requested_at"], "note": note}, ensure_ascii=False)) + from .services.precheck_state import mark_run_requested as service_mark_run_requested + + return service_mark_run_requested(note) def ack_analysis(note: str = ""): - state = load_state() - state["last_deep_analysis_at"] = utc_iso() - state["pending_trigger"] = False - state["pending_reasons"] = [] - state["last_ack_note"] = note - state.pop("run_requested_at", None) - state.pop("run_request_note", None) - save_state(state) - print(json.dumps({"ok": True, "acked_at": state["last_deep_analysis_at"], "note": note}, ensure_ascii=False)) + from .services.precheck_state import ack_analysis as service_ack_analysis + + return service_ack_analysis(note) def main(): diff --git a/src/coinhunter/services/precheck_analysis.py b/src/coinhunter/services/precheck_analysis.py new file mode 100644 index 0000000..57e3254 --- /dev/null +++ b/src/coinhunter/services/precheck_analysis.py @@ -0,0 +1,25 @@ +"""Analysis helpers for precheck.""" + +from __future__ import annotations + +from .. import precheck as precheck_module + + +def analyze_trigger(snapshot: dict, state: dict) -> dict: + return precheck_module.analyze_trigger(snapshot, state) + + +def build_failure_payload(exc: Exception) -> dict: + return { + "generated_at": precheck_module.utc_iso(), + "status": "deep_analysis_required", + "should_analyze": True, + "pending_trigger": True, + "cooldown_active": False, + "reasons": ["precheck-error"], + "hard_reasons": ["precheck-error"], + "soft_reasons": [], + "soft_score": 0, + "details": [str(exc)], + "compact_summary": f"预检查失败,转入深度分析兜底: {exc}", + } diff --git a/src/coinhunter/services/precheck_service.py b/src/coinhunter/services/precheck_service.py index b5a2c36..b68f509 100644 --- a/src/coinhunter/services/precheck_service.py +++ b/src/coinhunter/services/precheck_service.py @@ -5,39 +5,26 @@ from __future__ import annotations import json import sys -from .. import precheck as precheck_module +from . import precheck_analysis, precheck_snapshot, precheck_state def run(argv: list[str] | None = None) -> int: argv = list(sys.argv[1:] if argv is None else argv) if argv and argv[0] == "--ack": - precheck_module.ack_analysis(" ".join(argv[1:]).strip()) + precheck_state.ack_analysis(" ".join(argv[1:]).strip()) return 0 if argv and argv[0] == "--mark-run-requested": - precheck_module.mark_run_requested(" ".join(argv[1:]).strip()) + precheck_state.mark_run_requested(" ".join(argv[1:]).strip()) return 0 try: - state = precheck_module.sanitize_state_for_stale_triggers(precheck_module.load_state()) - snapshot = precheck_module.build_snapshot() - analysis = precheck_module.analyze_trigger(snapshot, state) - precheck_module.save_state(precheck_module.update_state_after_observation(state, snapshot, analysis)) + state = precheck_state.sanitize_state_for_stale_triggers(precheck_state.load_state()) + snapshot = precheck_snapshot.build_snapshot() + analysis = precheck_analysis.analyze_trigger(snapshot, state) + precheck_state.save_state(precheck_state.update_state_after_observation(state, snapshot, analysis)) print(json.dumps(analysis, ensure_ascii=False, indent=2)) return 0 except Exception as exc: - failure = { - "generated_at": precheck_module.utc_iso(), - "status": "deep_analysis_required", - "should_analyze": True, - "pending_trigger": True, - "cooldown_active": False, - "reasons": ["precheck-error"], - "hard_reasons": ["precheck-error"], - "soft_reasons": [], - "soft_score": 0, - "details": [str(exc)], - "compact_summary": f"预检查失败,转入深度分析兜底: {exc}", - } - print(json.dumps(failure, ensure_ascii=False, indent=2)) + print(json.dumps(precheck_analysis.build_failure_payload(exc), ensure_ascii=False, indent=2)) return 0 diff --git a/src/coinhunter/services/precheck_snapshot.py b/src/coinhunter/services/precheck_snapshot.py new file mode 100644 index 0000000..81ac936 --- /dev/null +++ b/src/coinhunter/services/precheck_snapshot.py @@ -0,0 +1,9 @@ +"""Snapshot construction helpers for precheck.""" + +from __future__ import annotations + +from .. import precheck as precheck_module + + +def build_snapshot() -> dict: + return precheck_module.build_snapshot() diff --git a/src/coinhunter/services/precheck_state.py b/src/coinhunter/services/precheck_state.py new file mode 100644 index 0000000..d05639b --- /dev/null +++ b/src/coinhunter/services/precheck_state.py @@ -0,0 +1,47 @@ +"""State helpers for precheck orchestration.""" + +from __future__ import annotations + +import json + +from .. import precheck as precheck_module + + +def load_state() -> dict: + return precheck_module.load_state() + + +def save_state(state: dict) -> None: + precheck_module.save_state(state) + + +def sanitize_state_for_stale_triggers(state: dict) -> dict: + return precheck_module.sanitize_state_for_stale_triggers(state) + + +def update_state_after_observation(state: dict, snapshot: dict, analysis: dict) -> dict: + return precheck_module.update_state_after_observation(state, snapshot, analysis) + + +def mark_run_requested(note: str = "") -> dict: + state = load_state() + state["run_requested_at"] = precheck_module.utc_iso() + state["run_request_note"] = note + save_state(state) + payload = {"ok": True, "run_requested_at": state["run_requested_at"], "note": note} + print(json.dumps(payload, ensure_ascii=False)) + return payload + + +def ack_analysis(note: str = "") -> dict: + state = load_state() + state["last_deep_analysis_at"] = precheck_module.utc_iso() + state["pending_trigger"] = False + state["pending_reasons"] = [] + state["last_ack_note"] = note + state.pop("run_requested_at", None) + state.pop("run_request_note", None) + save_state(state) + payload = {"ok": True, "acked_at": state["last_deep_analysis_at"], "note": note} + print(json.dumps(payload, ensure_ascii=False)) + return payload