Initial commit: TradeMind – Krypto-Trading-Bot mit Lernmodus
Per Podman deploybarer Bot, der Käufe und Verkäufe simuliert ausführt und sich aus den Ergebnissen weiter antrainiert. Aufbau - Einheitliche Bar-Verarbeitung für paper, backtest und live; ausgetauscht werden nur Datenquelle und Broker. - Börsenanbindung über ccxt: rund 100 Börsen allein über exchange.id erreichbar. Zugangsdaten kommen über ENV-Platzhalter, der Live-Modus ist doppelt abgesichert. - Paper-Broker mit Gebühren, Slippage, Börsenpräzision und Volumengrenzen. - Online trainierte logistische Regression bewertet jedes Einstiegssignal. Sie lernt aus realen Trade-Ergebnissen, aus Shadow-Labels aller Kandidaten – auch der abgelehnten – und aus Hintergrund-Stichproben; beim Kaltstart wird sie aus der Kurshistorie vorgelernt. - Risikomanagement: Positions- und Exposure-Grenzen, ATR-Stops, Cooldown sowie Tagesverlust- und Drawdown-Notbremsen. - SQLite-Persistenz, HTTP-Status mit Prometheus-Metriken und Dashboard, Webhooks. Deployment - Containerfile (zweistufig, non-root UID 10001), podman-compose, systemd-Quadlet. - Modell und Datenbank liegen im Volume /data und überleben Neustarts. 128 Tests, ruff sauber. Verifiziert gegen echte Marktdaten sowie im gebauten Container inklusive Healthcheck und Zustandswiederherstellung.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from trademind.config import LIVE_CONFIRMATION_PHRASE, Config, Mode, load_config
|
||||
|
||||
MINIMAL = """
|
||||
mode: paper
|
||||
market:
|
||||
symbols: [btc/usdt]
|
||||
"""
|
||||
|
||||
|
||||
def write(tmp_path, text: str):
|
||||
path = tmp_path / "config.yaml"
|
||||
path.write_text(text, encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def test_minimal_config_uses_defaults(tmp_path):
|
||||
config = load_config(write(tmp_path, MINIMAL))
|
||||
assert config.mode is Mode.PAPER
|
||||
assert config.market.symbols == ["BTC/USDT"] # normalisiert
|
||||
assert config.exchange.id == "binance"
|
||||
assert config.risk.max_open_positions == 3
|
||||
|
||||
|
||||
def test_env_placeholders_are_resolved(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("MY_KEY", "abc123")
|
||||
config = load_config(
|
||||
write(tmp_path, "mode: paper\nexchange:\n api_key: ${MY_KEY}\n api_secret: ${MISSING}\n")
|
||||
)
|
||||
assert config.exchange.api_key == "abc123"
|
||||
assert config.exchange.api_secret is None
|
||||
|
||||
|
||||
def test_env_placeholder_default_value(tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("TRADEMIND_EXCHANGE", raising=False)
|
||||
config = load_config(write(tmp_path, "exchange:\n id: ${TRADEMIND_EXCHANGE:-kraken}\n"))
|
||||
assert config.exchange.id == "kraken"
|
||||
|
||||
|
||||
def test_double_underscore_env_override(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("TRADEMIND__RISK__MAX_OPEN_POSITIONS", "7")
|
||||
monkeypatch.setenv("TRADEMIND__MARKET__SYMBOLS", "BTC/USDT,ETH/USDT")
|
||||
monkeypatch.setenv("TRADEMIND__EXCHANGE__SANDBOX", "false")
|
||||
config = load_config(write(tmp_path, MINIMAL))
|
||||
assert config.risk.max_open_positions == 7
|
||||
assert config.market.symbols == ["BTC/USDT", "ETH/USDT"]
|
||||
assert config.exchange.sandbox is False
|
||||
|
||||
|
||||
def test_live_mode_requires_confirmation(tmp_path):
|
||||
text = "mode: live\nexchange:\n api_key: k\n api_secret: s\n"
|
||||
with pytest.raises(ValueError, match="live_confirmation"):
|
||||
load_config(write(tmp_path, text))
|
||||
|
||||
|
||||
def test_live_mode_requires_credentials(tmp_path):
|
||||
text = f"mode: live\nlive_confirmation: {LIVE_CONFIRMATION_PHRASE}\n"
|
||||
with pytest.raises(ValueError, match="api_key"):
|
||||
load_config(write(tmp_path, text))
|
||||
|
||||
|
||||
def test_live_mode_accepted_when_complete(tmp_path):
|
||||
text = (
|
||||
f"mode: live\nlive_confirmation: {LIVE_CONFIRMATION_PHRASE}\n"
|
||||
"exchange:\n api_key: k\n api_secret: s\n"
|
||||
)
|
||||
config = load_config(write(tmp_path, text))
|
||||
assert config.mode is Mode.LIVE
|
||||
assert config.is_simulated is False
|
||||
|
||||
|
||||
def test_unknown_key_is_rejected(tmp_path):
|
||||
with pytest.raises(ValidationError):
|
||||
load_config(write(tmp_path, "mode: paper\nrisk:\n typo_here: 5\n"))
|
||||
|
||||
|
||||
def test_ema_periods_must_be_ordered(tmp_path):
|
||||
with pytest.raises(ValueError, match="fast_ema"):
|
||||
load_config(write(tmp_path, "strategy:\n rules:\n fast_ema: 30\n slow_ema: 10\n"))
|
||||
|
||||
|
||||
def test_missing_file_raises(tmp_path):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
load_config(tmp_path / "nope.yaml")
|
||||
|
||||
|
||||
def test_empty_symbols_rejected(tmp_path):
|
||||
with pytest.raises(ValidationError):
|
||||
load_config(write(tmp_path, "market:\n symbols: []\n"))
|
||||
|
||||
|
||||
def test_config_is_simulated_flag():
|
||||
assert Config(mode=Mode.PAPER).is_simulated
|
||||
assert Config(mode=Mode.BACKTEST).is_simulated
|
||||
Reference in New Issue
Block a user