refactor: split precheck state snapshot and analysis
This commit is contained in:
214
README.md
214
README.md
@@ -1,13 +1,69 @@
|
|||||||
# coinhunter-cli
|
# coinhunter-cli
|
||||||
|
|
||||||
CoinHunter CLI is the executable tooling layer for CoinHunter.
|
<p align="center">
|
||||||
|
<strong>The executable CLI layer for CoinHunter.</strong><br/>
|
||||||
|
Runtime-safe trading operations, precheck orchestration, review tooling, and market probes.
|
||||||
|
</p>
|
||||||
|
|
||||||
• Code lives in this repository.
|
<p align="center">
|
||||||
• User runtime data lives in ~/.coinhunter/ by default.
|
<img alt="Python" src="https://img.shields.io/badge/python-3.10%2B-blue" />
|
||||||
• Hermes skills can call this CLI instead of embedding large script collections.
|
<img alt="Status" src="https://img.shields.io/badge/status-active%20development-orange" />
|
||||||
• Runtime locations can be overridden with COINHUNTER_HOME, HERMES_HOME, COINHUNTER_ENV_FILE, and HERMES_BIN.
|
<img alt="Architecture" src="https://img.shields.io/badge/architecture-runtime%20%2B%20commands%20%2B%20services-6f42c1" />
|
||||||
|
</p>
|
||||||
|
|
||||||
## 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:
|
Editable install:
|
||||||
|
|
||||||
@@ -15,38 +71,152 @@ Editable install:
|
|||||||
pip install -e .
|
pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
## Core commands
|
Run directly after install:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
coinhunter --help
|
||||||
coinhunter --version
|
coinhunter --version
|
||||||
coinhunter doctor
|
```
|
||||||
coinhunter paths
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
Initialize user state:
|
||||||
|
|
||||||
|
```bash
|
||||||
coinhunter init
|
coinhunter init
|
||||||
|
```
|
||||||
|
|
||||||
|
Inspect runtime wiring:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
coinhunter paths
|
||||||
|
coinhunter doctor
|
||||||
|
```
|
||||||
|
|
||||||
|
Validate exchange credentials:
|
||||||
|
|
||||||
|
```bash
|
||||||
coinhunter check-api
|
coinhunter check-api
|
||||||
coinhunter smart-executor balances
|
```
|
||||||
|
|
||||||
|
Run precheck / gate plumbing:
|
||||||
|
|
||||||
|
```bash
|
||||||
coinhunter precheck
|
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-context 12
|
||||||
|
coinhunter review-engine 12
|
||||||
|
```
|
||||||
|
|
||||||
|
Probe external market data:
|
||||||
|
|
||||||
|
```bash
|
||||||
coinhunter market-probe bybit-ticker BTCUSDT
|
coinhunter market-probe bybit-ticker BTCUSDT
|
||||||
|
coinhunter market-probe bybit-klines BTCUSDT 60 20
|
||||||
```
|
```
|
||||||
|
|
||||||
## Runtime model
|
## Runtime model
|
||||||
|
|
||||||
Default layout:
|
Default layout:
|
||||||
|
|
||||||
• ~/.coinhunter/ stores config, positions, logs, reviews, and state.
|
```text
|
||||||
• ~/.hermes/.env stores exchange credentials unless COINHUNTER_ENV_FILE overrides it.
|
~/.coinhunter/
|
||||||
• hermes is discovered from PATH first, then ~/.local/bin/hermes, unless HERMES_BIN overrides it.
|
├── 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/
|
## Useful commands
|
||||||
• services/
|
|
||||||
• runtime/
|
|
||||||
• domain logic
|
|
||||||
|
|
||||||
The first split is already in place for:
|
### Diagnostics
|
||||||
|
|
||||||
• smart-executor -> commands.smart_executor + services.smart_executor_service
|
```bash
|
||||||
• precheck -> commands.precheck + services.precheck_service
|
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.
|
||||||
|
|||||||
@@ -904,23 +904,15 @@ def update_state_after_observation(state: dict, snapshot: dict, analysis: dict):
|
|||||||
|
|
||||||
|
|
||||||
def mark_run_requested(note: str = ""):
|
def mark_run_requested(note: str = ""):
|
||||||
state = load_state()
|
from .services.precheck_state import mark_run_requested as service_mark_run_requested
|
||||||
state["run_requested_at"] = utc_iso()
|
|
||||||
state["run_request_note"] = note
|
return service_mark_run_requested(note)
|
||||||
save_state(state)
|
|
||||||
print(json.dumps({"ok": True, "run_requested_at": state["run_requested_at"], "note": note}, ensure_ascii=False))
|
|
||||||
|
|
||||||
|
|
||||||
def ack_analysis(note: str = ""):
|
def ack_analysis(note: str = ""):
|
||||||
state = load_state()
|
from .services.precheck_state import ack_analysis as service_ack_analysis
|
||||||
state["last_deep_analysis_at"] = utc_iso()
|
|
||||||
state["pending_trigger"] = False
|
return service_ack_analysis(note)
|
||||||
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))
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
25
src/coinhunter/services/precheck_analysis.py
Normal file
25
src/coinhunter/services/precheck_analysis.py
Normal file
@@ -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}",
|
||||||
|
}
|
||||||
@@ -5,39 +5,26 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from .. import precheck as precheck_module
|
from . import precheck_analysis, precheck_snapshot, precheck_state
|
||||||
|
|
||||||
|
|
||||||
def run(argv: list[str] | None = None) -> int:
|
def run(argv: list[str] | None = None) -> int:
|
||||||
argv = list(sys.argv[1:] if argv is None else argv)
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
|
||||||
if argv and argv[0] == "--ack":
|
if argv and argv[0] == "--ack":
|
||||||
precheck_module.ack_analysis(" ".join(argv[1:]).strip())
|
precheck_state.ack_analysis(" ".join(argv[1:]).strip())
|
||||||
return 0
|
return 0
|
||||||
if argv and argv[0] == "--mark-run-requested":
|
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
|
return 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
state = precheck_module.sanitize_state_for_stale_triggers(precheck_module.load_state())
|
state = precheck_state.sanitize_state_for_stale_triggers(precheck_state.load_state())
|
||||||
snapshot = precheck_module.build_snapshot()
|
snapshot = precheck_snapshot.build_snapshot()
|
||||||
analysis = precheck_module.analyze_trigger(snapshot, state)
|
analysis = precheck_analysis.analyze_trigger(snapshot, state)
|
||||||
precheck_module.save_state(precheck_module.update_state_after_observation(state, snapshot, analysis))
|
precheck_state.save_state(precheck_state.update_state_after_observation(state, snapshot, analysis))
|
||||||
print(json.dumps(analysis, ensure_ascii=False, indent=2))
|
print(json.dumps(analysis, ensure_ascii=False, indent=2))
|
||||||
return 0
|
return 0
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
failure = {
|
print(json.dumps(precheck_analysis.build_failure_payload(exc), ensure_ascii=False, indent=2))
|
||||||
"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))
|
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
9
src/coinhunter/services/precheck_snapshot.py
Normal file
9
src/coinhunter/services/precheck_snapshot.py
Normal file
@@ -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()
|
||||||
47
src/coinhunter/services/precheck_state.py
Normal file
47
src/coinhunter/services/precheck_state.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user