from pathlib import Path import pytest from llm_trade_bot.config import ConfigError, load_config CONFIG = """ [deepseek] api_key = "deepseek-key" [bybit] api_key = "bybit-key" api_secret = "bybit-secret" [telegram] api_id = 123 api_hash = "api-hash" bot_token = "bot-token" admin_user_ids = [42] [brave] api_key = "brave-key" [prompts] file_path = "prompts.md" [schedule] timezone = "Europe/Warsaw" times = ["09:00", "21:00"] [scanner] categories = ["spot", "linear"] [risk] max_risk_per_trade_pct = 2 max_total_exposure_pct = 40 max_positions = 5 max_leverage = 3 [storage] database_path = "state.db" """ PROMPTS = """# Prompts ## master_prompt Master prompt. ## run_prompt Run prompt. """ def write_config(tmp_path: Path, content: str = CONFIG) -> Path: path = tmp_path / "config.toml" path.write_text(content, encoding="utf-8") (tmp_path / "prompts.md").write_text(PROMPTS, encoding="utf-8") return path def test_loads_and_resolves_database_path(tmp_path: Path): path = write_config(tmp_path) config = load_config(path) assert config.deepseek.model == "deepseek-flash" assert config.telegram.admin_user_ids == frozenset({42}) assert config.storage.database_path == tmp_path / "state.db" assert config.prompts.file_path == tmp_path / "prompts.md" def test_rejects_risk_above_hard_limit(tmp_path: Path): path = write_config(tmp_path, CONFIG.replace("max_leverage = 3", "max_leverage = 4")) with pytest.raises(ConfigError, match="max_leverage"): load_config(path) def test_rejects_non_official_deepseek_endpoint(tmp_path: Path): changed = CONFIG.replace("api_key = \"deepseek-key\"", 'api_key = "deepseek-key"\nbase_url = "https://example.com"', 1) path = write_config(tmp_path, changed) with pytest.raises(ConfigError, match="base_url"): load_config(path) def test_rejects_missing_prompts_file(tmp_path: Path): path = tmp_path / "config.toml" path.write_text(CONFIG, encoding="utf-8") with pytest.raises(ConfigError, match="Prompts file not found"): load_config(path) def test_legacy_bybit_config_selects_bybit(tmp_path: Path): config = load_config(write_config(tmp_path)) assert config.exchange.provider == "bybit" assert config.bybit is not None assert config.binance is None def test_binance_config_only_requires_binance_credentials(tmp_path: Path): content = CONFIG.replace( '[bybit]\napi_key = "bybit-key"\napi_secret = "bybit-secret"\n', '''[exchange] provider = "binance" [binance.spot] api_key = "spot-key" api_secret = "spot-secret" [binance.usdm] api_key = "usdm-key" api_secret = "usdm-secret" ''', ) config = load_config(write_config(tmp_path, content)) assert config.exchange.provider == "binance" assert config.bybit is None assert config.binance is not None assert config.binance.spot.api_key == "spot-key" def test_binance_rejects_endpoint_override(tmp_path: Path): content = CONFIG.replace( '[bybit]\napi_key = "bybit-key"\napi_secret = "bybit-secret"\n', '''[exchange] provider = "binance" [binance] base_url = "https://api.binance.com" [binance.spot] api_key = "spot-key" api_secret = "spot-secret" [binance.usdm] api_key = "usdm-key" api_secret = "usdm-secret" ''', ) with pytest.raises(ConfigError, match="fixed to Testnet"): load_config(write_config(tmp_path, content)) def test_binance_requires_both_selected_wallet_credentials(tmp_path: Path): content = CONFIG.replace( '[bybit]\napi_key = "bybit-key"\napi_secret = "bybit-secret"\n', '''[exchange] provider = "binance" [binance.spot] api_key = "spot-key" api_secret = "spot-secret" [binance.usdm] api_key = "replace-me" api_secret = "replace-me" ''', ) with pytest.raises(ConfigError, match="placeholder"): load_config(write_config(tmp_path, content))