diff --git a/README.md b/README.md index c852215..046abd5 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@
-
+
@@ -21,11 +21,25 @@
## Install
+For end users, install from PyPI with [pipx](https://pipx.pypa.io/) (recommended) to avoid polluting your system Python:
+
```bash
-pip install -e ".[dev]"
+pipx install coinhunter
coinhunter --help
```
+Check the installed version:
+
+```bash
+coinhunter --version
+```
+
+To update later:
+
+```bash
+pipx upgrade coinhunter
+```
+
## Initialize runtime
```bash
@@ -73,8 +87,13 @@ coinhunter trade futures close BTCUSDT
coinhunter opportunity portfolio
coinhunter opportunity scan
coinhunter opportunity scan --symbols BTCUSDT ETHUSDT SOLUSDT
+
+# Self-update
+coinhunter update
```
+`update` will try `pipx upgrade coinhunter` first, and fall back to `pip install --upgrade coinhunter` if pipx is not available.
+
## Architecture
CoinHunter V2 uses a flat, direct architecture:
@@ -106,6 +125,16 @@ Events include:
## Development
+Clone the repo and install in editable mode:
+
+```bash
+git clone https://git.tacitlab.cc/TacitLab/coinhunter-cli.git
+cd coinhunter-cli
+pip install -e ".[dev]"
+```
+
+Run quality checks:
+
```bash
pytest tests/ # run tests
ruff check src tests # lint
diff --git a/pyproject.toml b/pyproject.toml
index c676dfe..238ac0f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "coinhunter"
-version = "2.0.0"
+version = "2.0.1"
description = "Binance-first trading CLI for balances, market data, opportunity scanning, and execution."
readme = "README.md"
license = {text = "MIT"}
diff --git a/src/coinhunter/__init__.py b/src/coinhunter/__init__.py
index 23f8bf1..0e1d7d4 100644
--- a/src/coinhunter/__init__.py
+++ b/src/coinhunter/__init__.py
@@ -1,3 +1,8 @@
"""CoinHunter V2."""
-__version__ = "2.0.0"
+try:
+ from importlib.metadata import version
+
+ __version__ = version("coinhunter")
+except Exception: # pragma: no cover
+ __version__ = "unknown"
diff --git a/src/coinhunter/cli.py b/src/coinhunter/cli.py
index c5b1c32..c19f385 100644
--- a/src/coinhunter/cli.py
+++ b/src/coinhunter/cli.py
@@ -10,7 +10,7 @@ from . import __version__
from .binance.spot_client import SpotBinanceClient
from .binance.um_futures_client import UMFuturesClient
from .config import ensure_init_files, get_binance_credentials, load_config
-from .runtime import get_runtime_paths, print_json
+from .runtime import get_runtime_paths, print_json, self_update
from .services import account_service, market_service, opportunity_service, trade_service
@@ -102,6 +102,8 @@ def build_parser() -> argparse.ArgumentParser:
scan_parser = opportunity_subparsers.add_parser("scan")
scan_parser.add_argument("--symbols", nargs="*")
+ subparsers.add_parser("update", help="Upgrade coinhunter to the latest version")
+
return parser
@@ -231,6 +233,10 @@ def main(argv: list[str] | None = None) -> int:
return 0
parser.error("opportunity requires `portfolio` or `scan`")
+ if args.command == "update":
+ print_json(self_update())
+ return 0
+
parser.error(f"Unsupported command {args.command}")
return 2
except Exception as exc:
diff --git a/src/coinhunter/runtime.py b/src/coinhunter/runtime.py
index 45aaf7c..fdc80ba 100644
--- a/src/coinhunter/runtime.py
+++ b/src/coinhunter/runtime.py
@@ -4,6 +4,9 @@ from __future__ import annotations
import json
import os
+import shutil
+import subprocess
+import sys
from dataclasses import asdict, dataclass, is_dataclass
from datetime import date, datetime
from pathlib import Path
@@ -50,3 +53,17 @@ def json_default(value: Any) -> Any:
def print_json(payload: Any) -> None:
print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True, default=json_default))
+
+
+def self_update() -> dict[str, Any]:
+ if shutil.which("pipx"):
+ cmd = ["pipx", "upgrade", "coinhunter"]
+ else:
+ cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "coinhunter"]
+ result = subprocess.run(cmd, capture_output=True, text=True)
+ return {
+ "command": " ".join(cmd),
+ "returncode": result.returncode,
+ "stdout": result.stdout.strip(),
+ "stderr": result.stderr.strip(),
+ }
diff --git a/tests/test_cli.py b/tests/test_cli.py
index dd8d234..e9f13dc 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -36,3 +36,12 @@ class CLITestCase(unittest.TestCase):
result = cli.main(["market", "tickers", "BTCUSDT"])
self.assertEqual(result, 1)
self.assertIn("error: boom", stderr.getvalue())
+
+ def test_update_dispatches(self):
+ captured = {}
+ with patch.object(cli, "self_update", return_value={"command": "pipx upgrade coinhunter", "returncode": 0}), patch.object(
+ cli, "print_json", side_effect=lambda payload: captured.setdefault("payload", payload)
+ ):
+ result = cli.main(["update"])
+ self.assertEqual(result, 0)
+ self.assertEqual(captured["payload"]["returncode"], 0)