Harden trading, training, and monitoring
This commit is contained in:
@@ -154,6 +154,20 @@ class Settings:
|
||||
database_path: Path
|
||||
log_path: Path
|
||||
env_file_path: Path
|
||||
api_auth_token: str = ""
|
||||
training_worker_token: str = ""
|
||||
trusted_proxy_user_header: str = ""
|
||||
time_series_require_quality_gate: bool = False
|
||||
time_series_manual_quality_override: bool = False
|
||||
time_series_require_fresh_model: bool = False
|
||||
time_series_model_max_age_hours: float = 48.0
|
||||
market_ticker_max_age_seconds: float = 45.0
|
||||
live_order_fill_timeout_seconds: float = 20.0
|
||||
live_reconciliation_interval_seconds: float = 30.0
|
||||
live_protective_stop_enabled: bool = True
|
||||
hold_signal_sample_seconds: int = 60
|
||||
storage_retention_days: int = 30
|
||||
storage_prune_interval_seconds: int = 3600
|
||||
|
||||
@property
|
||||
def rest_base_url(self) -> str:
|
||||
@@ -289,7 +303,7 @@ def load_settings(env_file: str | Path | None = None) -> Settings:
|
||||
time_series_probe_min_edge_percent=_float_env("TIME_SERIES_PROBE_MIN_EDGE_PERCENT", 0.02),
|
||||
time_series_probe_min_probability_up=_float_env("TIME_SERIES_PROBE_MIN_PROBABILITY_UP", 0.55),
|
||||
time_series_probe_size_multiplier=_float_env("TIME_SERIES_PROBE_SIZE_MULTIPLIER", 0.40),
|
||||
time_series_rebound_fallback_enabled=_bool_env("TIME_SERIES_REBOUND_FALLBACK_ENABLED", True),
|
||||
time_series_rebound_fallback_enabled=_bool_env("TIME_SERIES_REBOUND_FALLBACK_ENABLED", False),
|
||||
stop_loss_percent=_float_env("STOP_LOSS_PERCENT", 0.04),
|
||||
stop_loss_exit_enabled=_bool_env("STOP_LOSS_EXIT_ENABLED", True),
|
||||
take_profit_percent=_float_env("TAKE_PROFIT_PERCENT", 0.035),
|
||||
@@ -307,7 +321,28 @@ def load_settings(env_file: str | Path | None = None) -> Settings:
|
||||
database_path=Path(os.getenv("DATABASE_PATH", "runtime/tradebot.sqlite3")),
|
||||
log_path=Path(os.getenv("LOG_PATH", "runtime/tradebot.log")),
|
||||
env_file_path=env_path,
|
||||
api_auth_token=os.getenv("TRADEBOT_API_TOKEN", "").strip(),
|
||||
training_worker_token=os.getenv("TRADEBOT_TRAINING_TOKEN", "").strip(),
|
||||
trusted_proxy_user_header=os.getenv("TRUSTED_PROXY_USER_HEADER", "").strip(),
|
||||
time_series_require_quality_gate=_bool_env(
|
||||
"TIME_SERIES_REQUIRE_QUALITY_GATE", strategy_mode == "torch_forecast"
|
||||
),
|
||||
time_series_manual_quality_override=_bool_env(
|
||||
"TIME_SERIES_MANUAL_QUALITY_OVERRIDE", False
|
||||
),
|
||||
time_series_require_fresh_model=_bool_env(
|
||||
"TIME_SERIES_REQUIRE_FRESH_MODEL", strategy_mode == "torch_forecast"
|
||||
),
|
||||
time_series_model_max_age_hours=_float_env("TIME_SERIES_MODEL_MAX_AGE_HOURS", 48.0),
|
||||
market_ticker_max_age_seconds=_float_env("MARKET_TICKER_MAX_AGE_SECONDS", 45.0),
|
||||
live_order_fill_timeout_seconds=_float_env("LIVE_ORDER_FILL_TIMEOUT_SECONDS", 20.0),
|
||||
live_reconciliation_interval_seconds=_float_env("LIVE_RECONCILIATION_INTERVAL_SECONDS", 30.0),
|
||||
live_protective_stop_enabled=_bool_env("LIVE_PROTECTIVE_STOP_ENABLED", True),
|
||||
hold_signal_sample_seconds=_int_env("HOLD_SIGNAL_SAMPLE_SECONDS", 60),
|
||||
storage_retention_days=_int_env("STORAGE_RETENTION_DAYS", 30),
|
||||
storage_prune_interval_seconds=_int_env("STORAGE_PRUNE_INTERVAL_SECONDS", 3600),
|
||||
)
|
||||
_validate_settings(settings)
|
||||
if settings.trading_mode == "live" and not settings.live_ready:
|
||||
raise ValueError(
|
||||
"Live mode is locked. Set ENABLE_LIVE_TRADING=true, "
|
||||
@@ -316,6 +351,36 @@ def load_settings(env_file: str | Path | None = None) -> Settings:
|
||||
return settings
|
||||
|
||||
|
||||
def _validate_settings(settings: Settings) -> None:
|
||||
errors: list[str] = []
|
||||
if not 1 <= settings.port <= 65535:
|
||||
errors.append("PORT must be in range 1..65535")
|
||||
if settings.starting_balance_usdt <= 0:
|
||||
errors.append("STARTING_BALANCE_USDT must be positive")
|
||||
if settings.min_position_usdt < 0:
|
||||
errors.append("MIN_POSITION_USDT must be non-negative")
|
||||
if settings.max_position_usdt < settings.min_position_usdt:
|
||||
errors.append("MAX_POSITION_USDT must be >= MIN_POSITION_USDT")
|
||||
if settings.max_symbol_exposure_usdt < settings.min_position_usdt:
|
||||
errors.append("MAX_SYMBOL_EXPOSURE_USDT must be >= MIN_POSITION_USDT")
|
||||
if settings.max_total_exposure_usdt < settings.max_symbol_exposure_usdt:
|
||||
errors.append("MAX_TOTAL_EXPOSURE_USDT must be >= MAX_SYMBOL_EXPOSURE_USDT")
|
||||
if settings.max_open_positions < 1 or settings.max_positions_per_symbol < 1:
|
||||
errors.append("position count limits must be positive")
|
||||
if settings.taker_fee_rate < 0 or settings.slippage_rate < 0:
|
||||
errors.append("TAKER_FEE_RATE and SLIPPAGE_RATE must be non-negative")
|
||||
if settings.market_ticker_max_age_seconds <= 0:
|
||||
errors.append("MARKET_TICKER_MAX_AGE_SECONDS must be positive")
|
||||
if settings.time_series_model_max_age_hours <= 0:
|
||||
errors.append("TIME_SERIES_MODEL_MAX_AGE_HOURS must be positive")
|
||||
if settings.live_order_fill_timeout_seconds <= 0:
|
||||
errors.append("LIVE_ORDER_FILL_TIMEOUT_SECONDS must be positive")
|
||||
if settings.live_reconciliation_interval_seconds <= 0:
|
||||
errors.append("LIVE_RECONCILIATION_INTERVAL_SECONDS must be positive")
|
||||
if errors:
|
||||
raise ValueError("; ".join(errors))
|
||||
|
||||
|
||||
def update_env_value(path: Path, key: str, value: str) -> None:
|
||||
lines = path.read_text(encoding="utf-8").splitlines() if path.exists() else []
|
||||
output: list[str] = []
|
||||
|
||||
Reference in New Issue
Block a user