- 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>
108 lines
3.8 KiB
Python
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())
|