Files
coinhunter-cli/src/coinhunter/services/precheck_core.py
Tacit Lab 62c40a9776 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>
2026-04-16 01:21:27 +08:00

108 lines
3.8 KiB
Python

"""Backward-compatible facade for precheck internals.
The reusable implementation has been split into smaller modules:
- precheck_constants : paths and thresholds
- time_utils : UTC/local time helpers
- data_utils : json, hash, float, symbol normalization
- state_manager : load/save/sanitize state
- market_data : exchange, ohlcv, metrics
- candidate_scoring : top candidate selection
- snapshot_builder : build_snapshot
- adaptive_profile : trigger profile builder
- trigger_analyzer : analyze_trigger
Keep this module importable so older entrypoints continue to work.
"""
from __future__ import annotations
from importlib import import_module
from ..runtime import get_runtime_paths
_PATH_ALIASES = {
"PATHS": lambda: get_runtime_paths(),
"BASE_DIR": lambda: get_runtime_paths().root,
"STATE_DIR": lambda: get_runtime_paths().state_dir,
"STATE_FILE": lambda: get_runtime_paths().precheck_state_file,
"POSITIONS_FILE": lambda: get_runtime_paths().positions_file,
"CONFIG_FILE": lambda: get_runtime_paths().config_file,
"ENV_FILE": lambda: get_runtime_paths().env_file,
}
_MODULE_MAP = {
"BASE_PRICE_MOVE_TRIGGER_PCT": ".precheck_constants",
"BASE_PNL_TRIGGER_PCT": ".precheck_constants",
"BASE_PORTFOLIO_MOVE_TRIGGER_PCT": ".precheck_constants",
"BASE_CANDIDATE_SCORE_TRIGGER_RATIO": ".precheck_constants",
"BASE_FORCE_ANALYSIS_AFTER_MINUTES": ".precheck_constants",
"BASE_COOLDOWN_MINUTES": ".precheck_constants",
"TOP_CANDIDATES": ".precheck_constants",
"MIN_ACTIONABLE_USDT": ".precheck_constants",
"MIN_REAL_POSITION_VALUE_USDT": ".precheck_constants",
"BLACKLIST": ".precheck_constants",
"HARD_STOP_PCT": ".precheck_constants",
"HARD_MOON_PCT": ".precheck_constants",
"MIN_CHANGE_PCT": ".precheck_constants",
"MAX_PRICE_CAP": ".precheck_constants",
"HARD_REASON_DEDUP_MINUTES": ".precheck_constants",
"MAX_PENDING_TRIGGER_MINUTES": ".precheck_constants",
"MAX_RUN_REQUEST_MINUTES": ".precheck_constants",
"utc_now": ".time_utils",
"utc_iso": ".time_utils",
"parse_ts": ".time_utils",
"get_local_now": ".time_utils",
"session_label": ".time_utils",
"load_json": ".data_utils",
"stable_hash": ".data_utils",
"to_float": ".data_utils",
"norm_symbol": ".data_utils",
"load_env": ".state_manager",
"load_positions": ".state_manager",
"load_state": ".state_manager",
"load_config": ".state_manager",
"clear_run_request_fields": ".state_manager",
"sanitize_state_for_stale_triggers": ".state_manager",
"save_state": ".state_manager",
"update_state_after_observation": ".state_manager",
"get_exchange": ".market_data",
"fetch_ohlcv_batch": ".market_data",
"compute_ohlcv_metrics": ".market_data",
"enrich_candidates_and_positions": ".market_data",
"regime_from_pct": ".market_data",
"_liquidity_score": ".candidate_scoring",
"_breakout_score": ".candidate_scoring",
"top_candidates_from_tickers": ".candidate_scoring",
"build_snapshot": ".snapshot_builder",
"build_adaptive_profile": ".adaptive_profile",
"_candidate_weight": ".adaptive_profile",
"analyze_trigger": ".trigger_analyzer",
}
__all__ = sorted(set(_MODULE_MAP) | set(_PATH_ALIASES) | {"main"})
def __getattr__(name: str):
if name in _PATH_ALIASES:
return _PATH_ALIASES[name]()
if name not in _MODULE_MAP:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
module_name = _MODULE_MAP[name]
module = import_module(module_name, __package__)
return getattr(module, name)
def __dir__():
return sorted(set(globals()) | set(__all__))
def main():
import sys
from .precheck_service import run as _run_service
return _run_service(sys.argv[1:])
if __name__ == "__main__":
raise SystemExit(main())