refactor: address high-priority debt and publish to PyPI

- Fix TOCTOU race conditions by wrapping read-modify-write cycles
  under single-file locks in execution_state, portfolio_service,
  precheck_state, state_manager, and precheck_service.
- Add missing test coverage (96 tests total):
  - test_review_service.py (15 tests)
  - test_check_api.py (6 tests)
  - test_external_gate.py main branches (+10 tests)
  - test_trade_execution.py new commands (+8 tests)
- Unify all agent-consumed JSON messages to English.
- Config-ize hardcoded values (volume filter, schema_version) via
  get_user_config with sensible defaults.
- Add 1-hour TTL to exchange cache with force_new override.
- Add ruff and mypy to dev dependencies; fix all type errors.
- Add __all__ declarations to 11 service modules.
- Sync README with new commands, config tuning docs, and PyPI badge.
- Publish package as coinhunter==1.0.0 on PyPI with MIT license.
- Deprecate coinhunter-cli==1.0.1 with runtime warning.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-16 01:21:27 +08:00
parent 01bb54dee5
commit 62c40a9776
53 changed files with 2338 additions and 671 deletions

View File

@@ -1,17 +1,25 @@
"""Execution state helpers (decision deduplication, executions.json)."""
import hashlib
from ..runtime import get_runtime_paths
from .file_utils import load_json_locked, save_json_locked
from .trade_common import bj_now_iso
__all__ = [
"default_decision_id",
"load_executions",
"save_executions",
"record_execution_state",
"get_execution_state",
]
PATHS = get_runtime_paths()
EXECUTIONS_FILE = PATHS.executions_file
EXECUTIONS_LOCK = PATHS.executions_lock
from ..runtime import get_runtime_paths
from .file_utils import load_json_locked, read_modify_write_json, save_json_locked
def _paths():
return get_runtime_paths()
def default_decision_id(action: str, argv_tail: list[str]) -> str:
from datetime import datetime
from .trade_common import CST
now = datetime.now(CST)
@@ -22,17 +30,24 @@ def default_decision_id(action: str, argv_tail: list[str]) -> str:
def load_executions() -> dict:
return load_json_locked(EXECUTIONS_FILE, EXECUTIONS_LOCK, {"executions": {}}).get("executions", {})
paths = _paths()
data = load_json_locked(paths.executions_file, paths.executions_lock, {"executions": {}})
return data.get("executions", {}) # type: ignore[no-any-return]
def save_executions(executions: dict):
save_json_locked(EXECUTIONS_FILE, EXECUTIONS_LOCK, {"executions": executions})
paths = _paths()
save_json_locked(paths.executions_file, paths.executions_lock, {"executions": executions})
def record_execution_state(decision_id: str, payload: dict):
executions = load_executions()
executions[decision_id] = payload
save_executions(executions)
paths = _paths()
read_modify_write_json(
paths.executions_file,
paths.executions_lock,
{"executions": {}},
lambda data: data.setdefault("executions", {}).__setitem__(decision_id, payload) or data,
)
def get_execution_state(decision_id: str):