- 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>
106 lines
3.9 KiB
Python
106 lines
3.9 KiB
Python
"""Adaptive trigger profile builder for precheck."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from .data_utils import to_float
|
|
from .precheck_constants import (
|
|
BASE_CANDIDATE_SCORE_TRIGGER_RATIO,
|
|
BASE_COOLDOWN_MINUTES,
|
|
BASE_FORCE_ANALYSIS_AFTER_MINUTES,
|
|
BASE_PNL_TRIGGER_PCT,
|
|
BASE_PORTFOLIO_MOVE_TRIGGER_PCT,
|
|
BASE_PRICE_MOVE_TRIGGER_PCT,
|
|
MIN_ACTIONABLE_USDT,
|
|
MIN_REAL_POSITION_VALUE_USDT,
|
|
)
|
|
|
|
|
|
def build_adaptive_profile(snapshot: dict):
|
|
portfolio_value = snapshot.get("portfolio_value_usdt", 0)
|
|
free_usdt = snapshot.get("free_usdt", 0)
|
|
session = snapshot.get("session")
|
|
market = snapshot.get("market_regime", {})
|
|
volatility_score = to_float(market.get("volatility_score"), 0)
|
|
leader_score = to_float(market.get("leader_score"), 0)
|
|
actionable_positions = int(snapshot.get("actionable_positions") or 0)
|
|
largest_position_value = to_float(snapshot.get("largest_position_value_usdt"), 0)
|
|
|
|
capital_band = "micro" if portfolio_value < 25 else "small" if portfolio_value < 100 else "normal"
|
|
session_mode = "quiet" if session in {"overnight", "asia-morning"} else "active"
|
|
volatility_mode = "high" if volatility_score >= 2.5 or leader_score >= 120 else "normal"
|
|
dust_mode = free_usdt < MIN_ACTIONABLE_USDT and largest_position_value < MIN_REAL_POSITION_VALUE_USDT
|
|
|
|
price_trigger = BASE_PRICE_MOVE_TRIGGER_PCT
|
|
pnl_trigger = BASE_PNL_TRIGGER_PCT
|
|
portfolio_trigger = BASE_PORTFOLIO_MOVE_TRIGGER_PCT
|
|
candidate_ratio = BASE_CANDIDATE_SCORE_TRIGGER_RATIO
|
|
force_minutes = BASE_FORCE_ANALYSIS_AFTER_MINUTES
|
|
cooldown_minutes = BASE_COOLDOWN_MINUTES
|
|
soft_score_threshold = 2.0
|
|
|
|
if capital_band == "micro":
|
|
price_trigger += 0.02
|
|
pnl_trigger += 0.03
|
|
portfolio_trigger += 0.04
|
|
candidate_ratio += 0.25
|
|
force_minutes += 180
|
|
cooldown_minutes += 30
|
|
soft_score_threshold += 1.0
|
|
elif capital_band == "small":
|
|
price_trigger += 0.01
|
|
pnl_trigger += 0.01
|
|
portfolio_trigger += 0.01
|
|
candidate_ratio += 0.1
|
|
force_minutes += 60
|
|
cooldown_minutes += 10
|
|
soft_score_threshold += 0.5
|
|
|
|
if session_mode == "quiet":
|
|
price_trigger += 0.01
|
|
pnl_trigger += 0.01
|
|
portfolio_trigger += 0.01
|
|
candidate_ratio += 0.05
|
|
soft_score_threshold += 0.5
|
|
else:
|
|
force_minutes = max(120, force_minutes - 30)
|
|
|
|
if volatility_mode == "high":
|
|
price_trigger = max(0.02, price_trigger - 0.01)
|
|
pnl_trigger = max(0.025, pnl_trigger - 0.005)
|
|
portfolio_trigger = max(0.025, portfolio_trigger - 0.005)
|
|
candidate_ratio = max(1.1, candidate_ratio - 0.1)
|
|
cooldown_minutes = max(20, cooldown_minutes - 10)
|
|
soft_score_threshold = max(1.0, soft_score_threshold - 0.5)
|
|
|
|
if dust_mode:
|
|
candidate_ratio += 0.3
|
|
force_minutes += 180
|
|
cooldown_minutes += 30
|
|
soft_score_threshold += 1.5
|
|
|
|
return {
|
|
"capital_band": capital_band,
|
|
"session_mode": session_mode,
|
|
"volatility_mode": volatility_mode,
|
|
"dust_mode": dust_mode,
|
|
"price_move_trigger_pct": round(price_trigger, 4),
|
|
"pnl_trigger_pct": round(pnl_trigger, 4),
|
|
"portfolio_move_trigger_pct": round(portfolio_trigger, 4),
|
|
"candidate_score_trigger_ratio": round(candidate_ratio, 4),
|
|
"force_analysis_after_minutes": int(force_minutes),
|
|
"cooldown_minutes": int(cooldown_minutes),
|
|
"soft_score_threshold": round(soft_score_threshold, 2),
|
|
"new_entries_allowed": free_usdt >= MIN_ACTIONABLE_USDT and not dust_mode,
|
|
"switching_allowed": actionable_positions > 0 or portfolio_value >= 25,
|
|
}
|
|
|
|
|
|
def _candidate_weight(snapshot: dict, profile: dict) -> float:
|
|
if not profile.get("new_entries_allowed"):
|
|
return 0.5
|
|
if profile.get("volatility_mode") == "high":
|
|
return 1.5
|
|
if snapshot.get("session") in {"europe-open", "us-session"}:
|
|
return 1.25
|
|
return 1.0
|